Merge "Default to hardware vibration attribute for SCROLL_* feedback" into main
diff --git a/core/java/android/app/appfunctions/AppFunctionManagerConfiguration.java b/core/java/android/app/appfunctions/AppFunctionManagerConfiguration.java
index fa77e79..cb21d1f 100644
--- a/core/java/android/app/appfunctions/AppFunctionManagerConfiguration.java
+++ b/core/java/android/app/appfunctions/AppFunctionManagerConfiguration.java
@@ -20,7 +20,6 @@
import android.annotation.NonNull;
import android.content.Context;
-import android.content.pm.PackageManager;
/**
* Represents the system configuration of support for the {@code AppFunctionManager} and associated
@@ -29,15 +28,13 @@
* @hide
*/
public class AppFunctionManagerConfiguration {
- private final Context mContext;
-
/**
* Constructs a new instance of {@code AppFunctionManagerConfiguration}.
*
* @param context context
*/
public AppFunctionManagerConfiguration(@NonNull final Context context) {
- mContext = context;
+ // Context can be used to access system features, etc.
}
/**
@@ -46,7 +43,7 @@
* @return {@code true} if supported; otherwise {@code false}
*/
public boolean isSupported() {
- return enableAppFunctionManager() && !isWatch();
+ return enableAppFunctionManager();
}
/**
@@ -58,8 +55,4 @@
public static boolean isSupported(@NonNull final Context context) {
return new AppFunctionManagerConfiguration(context).isSupported();
}
-
- private boolean isWatch() {
- return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
- }
}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index ff0a3dd..7de7131 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -68,6 +68,16 @@
}
flag {
+ name: "multiple_alarm_notifications_support"
+ namespace: "multiuser"
+ description: "Implement handling of multiple simultaneous alarms/timers on bg users"
+ bug: "367615180"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_biometrics_to_unlock_private_space"
is_exported: true
namespace: "profile_experiences"
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 229e8ee7..4f74198 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -221,13 +221,13 @@
@Override
public boolean setControl(@Nullable InsetsSourceControl control, int[] showTypes,
- int[] hideTypes) {
+ int[] hideTypes, int[] cancelTypes) {
if (Flags.refactorInsetsController()) {
- return super.setControl(control, showTypes, hideTypes);
+ return super.setControl(control, showTypes, hideTypes, cancelTypes);
} else {
ImeTracing.getInstance().triggerClientDump("ImeInsetsSourceConsumer#setControl",
mController.getHost().getInputMethodManager(), null /* icProto */);
- if (!super.setControl(control, showTypes, hideTypes)) {
+ if (!super.setControl(control, showTypes, hideTypes, cancelTypes)) {
return false;
}
if (control == null && !mIsRequestedVisibleAwaitingLeash) {
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 97facc1..4fead2a 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -371,6 +371,7 @@
mPendingInsets = mLayoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN
? mShownInsets : mHiddenInsets;
mPendingAlpha = 1f;
+ mPendingFraction = 1f;
applyChangeInsets(null);
mCancelled = true;
mListener.onCancelled(mReadyDispatched ? this : null);
@@ -486,6 +487,17 @@
if (controls == null) {
return;
}
+
+ final boolean visible = mPendingFraction == 0
+ // The first frame of ANIMATION_TYPE_SHOW should be invisible since it is
+ // animated from the hidden state.
+ ? mAnimationType != ANIMATION_TYPE_SHOW
+ : mPendingFraction < 1f || (mFinished
+ ? mShownOnFinish
+ // If the animation is cancelled, mFinished and mShownOnFinish are not set.
+ // Here uses mLayoutInsetsDuringAnimation to decide if it should be visible.
+ : mLayoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN);
+
// TODO: Implement behavior when inset spans over multiple types
for (int i = controls.size() - 1; i >= 0; i--) {
final InsetsSourceControl control = controls.valueAt(i);
@@ -498,12 +510,6 @@
}
addTranslationToMatrix(side, offset, mTmpMatrix, mTmpFrame);
- // The first frame of ANIMATION_TYPE_SHOW should be invisible since it is animated from
- // the hidden state.
- final boolean visible = mPendingFraction == 0
- ? mAnimationType != ANIMATION_TYPE_SHOW
- : !mFinished || mShownOnFinish;
-
if (outState != null && source != null) {
outState.addSource(new InsetsSource(source)
.setVisible(visible)
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 8ac5532..d08873c 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -957,6 +957,7 @@
int consumedControlCount = 0;
final @InsetsType int[] showTypes = new int[1];
final @InsetsType int[] hideTypes = new int[1];
+ final @InsetsType int[] cancelTypes = new int[1];
ImeTracker.Token statsToken = null;
// Ensure to update all existing source consumers
@@ -982,7 +983,7 @@
// control may be null, but we still need to update the control to null if it got
// revoked.
- consumer.setControl(control, showTypes, hideTypes);
+ consumer.setControl(control, showTypes, hideTypes, cancelTypes);
}
// Ensure to create source consumers if not available yet.
@@ -990,7 +991,7 @@
for (int i = mTmpControlArray.size() - 1; i >= 0; i--) {
final InsetsSourceControl control = mTmpControlArray.valueAt(i);
getSourceConsumer(control.getId(), control.getType())
- .setControl(control, showTypes, hideTypes);
+ .setControl(control, showTypes, hideTypes, cancelTypes);
}
}
@@ -1002,6 +1003,10 @@
}
mTmpControlArray.clear();
+ if (cancelTypes[0] != 0) {
+ cancelExistingControllers(cancelTypes[0]);
+ }
+
// Do not override any animations that the app started in the OnControllableInsetsChanged
// listeners.
int animatingTypes = invokeControllableInsetsChangedListeners();
@@ -2154,12 +2159,12 @@
new InsetsSourceControl(ID_IME_CAPTION_BAR, captionBar(),
null /* leash */, false /* initialVisible */,
new Point(), Insets.NONE),
- new int[1], new int[1]);
+ new int[1], new int[1], new int[1]);
} else {
mState.removeSource(ID_IME_CAPTION_BAR);
InsetsSourceConsumer sourceConsumer = mSourceConsumers.get(ID_IME_CAPTION_BAR);
if (sourceConsumer != null) {
- sourceConsumer.setControl(null, new int[1], new int[1]);
+ sourceConsumer.setControl(null, new int[1], new int[1], new int[1]);
}
}
mHost.notifyInsetsChanged();
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index da788a7..17f33c1 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -122,7 +122,7 @@
/**
* Updates the control delivered from the server.
-
+ *
* @param showTypes An integer array with a single entry that determines which types a show
* animation should be run after setting the control.
* @param hideTypes An integer array with a single entry that determines which types a hide
@@ -130,7 +130,7 @@
* @return Whether the control has changed from the server
*/
public boolean setControl(@Nullable InsetsSourceControl control,
- @InsetsType int[] showTypes, @InsetsType int[] hideTypes) {
+ @InsetsType int[] showTypes, @InsetsType int[] hideTypes, int[] cancelTypes) {
if (Objects.equals(mSourceControl, control)) {
if (mSourceControl != null && mSourceControl != control) {
mSourceControl.release(SurfaceControl::release);
@@ -165,6 +165,12 @@
// Reset the applier to the default one which has the most lightweight implementation.
setSurfaceParamsApplier(InsetsAnimationControlRunner.SurfaceParamsApplier.DEFAULT);
} else {
+ if (lastControl != null && InsetsSource.getInsetSide(lastControl.getInsetsHint())
+ != InsetsSource.getInsetSide(control.getInsetsHint())) {
+ // The source has been moved to a different side. The coordinates are stale.
+ // Canceling existing animation if there is any.
+ cancelTypes[0] |= mType;
+ }
final boolean requestedVisible = isRequestedVisibleAwaitingControl();
final SurfaceControl oldLeash = lastControl != null ? lastControl.getLeash() : null;
final SurfaceControl newLeash = control.getLeash();
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 27417c0..8bd7078 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -20,7 +20,9 @@
#include <android_runtime/AndroidRuntime.h>
#include <jni_wrappers.h>
#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
#include <nativehelper/jni_macros.h>
+#include <unicode/locid.h>
#include <unicode/putil.h>
#include <unicode/udata.h>
@@ -64,8 +66,8 @@
};
int register_libcore_util_NativeAllocationRegistry(JNIEnv* env) {
- return jniRegisterNativeMethods(env, "libcore/util/NativeAllocationRegistry", gMethods,
- NELEM(gMethods));
+ return android::RegisterMethodsOrDie(env, "libcore/util/NativeAllocationRegistry", gMethods,
+ NELEM(gMethods));
}
namespace android {
@@ -259,35 +261,67 @@
#endif
}
-// Loads the ICU data file from the location specified in the system property ro.icu.data.path
-static void loadIcuData() {
- string icuPath = base::GetProperty("ro.icu.data.path", "");
- if (!icuPath.empty()) {
- // Set the location of ICU data
- void* addr = mmapFile(icuPath.c_str());
- UErrorCode err = U_ZERO_ERROR;
- udata_setCommonData(addr, &err);
- if (err != U_ZERO_ERROR) {
- ALOGE("Unable to load ICU data\n");
- }
- }
-}
-
-static int register_android_core_classes(JNIEnv* env) {
+// returns result from java.lang.System.getProperty
+static string getJavaProperty(JNIEnv* env, const char* property_name) {
jclass system = FindClassOrDie(env, "java/lang/System");
jmethodID getPropertyMethod =
GetStaticMethodIDOrDie(env, system, "getProperty",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
- // Get the names of classes that need to register their native methods
- auto nativesClassesJString =
- (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
- env->NewStringUTF("core_native_classes"),
- env->NewStringUTF(""));
- const char* nativesClassesArray = env->GetStringUTFChars(nativesClassesJString, nullptr);
- string nativesClassesString(nativesClassesArray);
+ auto jString = (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
+ env->NewStringUTF(property_name),
+ env->NewStringUTF(""));
+ ScopedUtfChars chars(env, jString);
+ return string(chars.c_str());
+}
+
+static void loadIcuData(string icuPath) {
+ void* addr = mmapFile(icuPath.c_str());
+ UErrorCode err = U_ZERO_ERROR;
+ udata_setCommonData(addr, &err);
+ if (err != U_ZERO_ERROR) {
+ ALOGE("Unable to load ICU data\n");
+ }
+}
+
+// Loads the ICU data file from the location specified in properties.
+// First try specified in the system property ro.icu.data.path,
+// then fallback to java property icu.data.path
+static void loadIcuData() {
+ JNIEnv* env = AndroidRuntime::getJNIEnv();
+ string icuPath = base::GetProperty("ro.icu.data.path", "");
+ if (!icuPath.empty()) {
+ loadIcuData(icuPath);
+ } else {
+ // fallback to read from java.lang.System.getProperty
+ string icuPathFromJava = getJavaProperty(env, "icu.data.path");
+ if (!icuPathFromJava.empty()) {
+ loadIcuData(icuPathFromJava);
+ }
+ }
+
+ // Check for the ICU default locale property. In Libcore, the default ICU
+ // locale is set when ICU.setDefaultLocale is called, which is called by
+ // Libcore's implemenentation of Java's Locale.setDefault. The default
+ // locale is used in cases such as when ucol_open(NULL, ...) is called, for
+ // example in SQLite's 'COLLATE UNICODE'.
+ string icuLocaleDefault = getJavaProperty(env, "icu.locale.default");
+ if (!icuLocaleDefault.empty()) {
+ UErrorCode status = U_ZERO_ERROR;
+ icu::Locale locale = icu::Locale::forLanguageTag(icuLocaleDefault.c_str(), status);
+ if (U_SUCCESS(status)) {
+ icu::Locale::setDefault(locale, status);
+ }
+ if (U_FAILURE(status)) {
+ fprintf(stderr, "Failed to set the ICU default locale to '%s' (error code %d)\n",
+ icuLocaleDefault.c_str(), status);
+ }
+ }
+}
+
+static int register_android_core_classes(JNIEnv* env) {
+ string nativesClassesString = getJavaProperty(env, "core_native_classes");
vector<string> classesToRegister = parseCsv(nativesClassesString);
- env->ReleaseStringUTFChars(nativesClassesJString, nativesClassesArray);
if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) {
return JNI_ERR;
@@ -359,6 +393,11 @@
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
+
+ auto method_binding_format = getJavaProperty(env, "method_binding_format");
+
+ setJniMethodFormat(method_binding_format);
+
// Register native functions.
if (startReg(env) < 0) {
ALOGE("Unable to register all android native methods\n");
@@ -391,7 +430,6 @@
} // namespace android
-#ifndef _WIN32
using namespace android;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -400,12 +438,14 @@
return JNI_ERR;
}
- Vector<String8> args;
- HostRuntime runtime;
+ string useBaseHostRuntime = getJavaProperty(env, "use_base_native_hostruntime");
+ if (useBaseHostRuntime == "true") {
+ Vector<String8> args;
+ HostRuntime runtime;
- runtime.onVmCreated(env);
- runtime.start("HostRuntime", args, false);
+ runtime.onVmCreated(env);
+ runtime.start("HostRuntime", args, false);
+ }
return JNI_VERSION_1_6;
}
-#endif
diff --git a/core/res/res/layout/input_method_switch_dialog_new.xml b/core/res/res/layout/input_method_switch_dialog_new.xml
index 058fe3f..118f93b 100644
--- a/core/res/res/layout/input_method_switch_dialog_new.xml
+++ b/core/res/res/layout/input_method_switch_dialog_new.xml
@@ -39,7 +39,7 @@
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingVertical="8dp"
+ android:paddingTop="8dp"
android:clipToPadding="false"
android:layoutManager="com.android.internal.widget.LinearLayoutManager"/>
@@ -74,8 +74,7 @@
android:text="@string/input_method_switcher_settings_button"
android:fontFamily="google-sans-text"
android:textAppearance="?attr/textAppearance"
- android:contentDescription="@string/input_method_language_settings"
- android:visibility="gone"/>
+ android:contentDescription="@string/input_method_language_settings"/>
</LinearLayout>
diff --git a/core/res/res/layout/input_method_switch_item_divider.xml b/core/res/res/layout/input_method_switch_item_divider.xml
new file mode 100644
index 0000000..4f8c963
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_item_divider.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="16dp">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?attr/materialColorOutlineVariant"
+ android:layout_marginStart="20dp"
+ android:layout_marginEnd="24dp"
+ android:importantForAccessibility="no"/>
+
+</LinearLayout>
diff --git a/core/res/res/layout/input_method_switch_item_header.xml b/core/res/res/layout/input_method_switch_item_header.xml
new file mode 100644
index 0000000..f0080a6
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_item_header.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="16dp">
+
+ <TextView
+ android:id="@+id/header_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="8dp"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:fontFamily="google-sans-text"
+ android:textAppearance="?attr/textAppearance"
+ android:accessibilityHeading="true"
+ android:textColor="?attr/materialColorPrimary"/>
+
+</LinearLayout>
diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml
index 10d938c..f8710cc 100644
--- a/core/res/res/layout/input_method_switch_item_new.xml
+++ b/core/res/res/layout/input_method_switch_item_new.xml
@@ -16,76 +16,45 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list_item"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:paddingHorizontal="16dp"
- android:paddingBottom="8dp">
-
- <View
- android:id="@+id/divider"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:background="?attr/materialColorSurfaceVariant"
- android:layout_marginStart="20dp"
- android:layout_marginTop="8dp"
- android:layout_marginEnd="24dp"
- android:layout_marginBottom="12dp"
- android:visibility="gone"
- android:importantForAccessibility="no"/>
-
- <TextView
- android:id="@+id/header_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="8dp"
- android:ellipsize="end"
- android:singleLine="true"
- android:fontFamily="google-sans-text"
- android:textAppearance="?attr/textAppearance"
- android:textColor="?attr/materialColorPrimary"
- android:visibility="gone"/>
+ android:layout_height="72dp"
+ android:background="@drawable/input_method_switch_item_background"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginBottom="8dp"
+ android:paddingStart="20dp"
+ android:paddingEnd="24dp">
<LinearLayout
- android:id="@+id/list_item"
- android:layout_width="match_parent"
- android:layout_height="72dp"
- android:background="@drawable/input_method_switch_item_background"
- android:gravity="center_vertical"
- android:orientation="horizontal"
- android:paddingStart="20dp"
- android:paddingEnd="24dp">
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="start|center_vertical"
+ android:orientation="vertical">
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:gravity="start|center_vertical"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="marquee"
- android:singleLine="true"
- android:fontFamily="google-sans-text"
- android:textColor="@color/input_method_switch_on_item"
- android:textAppearance="?attr/textAppearanceListItem"/>
-
- </LinearLayout>
-
- <ImageView
- android:id="@+id/image"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:gravity="center_vertical"
- android:layout_marginStart="12dp"
- android:src="@drawable/ic_check_24dp"
- android:tint="@color/input_method_switch_on_item"
- android:visibility="gone"
- android:importantForAccessibility="no"/>
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:fontFamily="google-sans-text"
+ android:textColor="@color/input_method_switch_on_item"
+ android:textAppearance="?attr/textAppearanceListItem"/>
</LinearLayout>
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:gravity="center_vertical"
+ android:layout_marginStart="12dp"
+ android:src="@drawable/ic_check_24dp"
+ android:tint="@color/input_method_switch_on_item"
+ android:visibility="gone"
+ android:importantForAccessibility="no"/>
+
</LinearLayout>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c1893ab..4f63fac 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1582,6 +1582,8 @@
<java-symbol type="layout" name="input_method" />
<java-symbol type="layout" name="input_method_extract_view" />
<java-symbol type="layout" name="input_method_switch_item" />
+ <java-symbol type="layout" name="input_method_switch_item_divider" />
+ <java-symbol type="layout" name="input_method_switch_item_header" />
<java-symbol type="layout" name="input_method_switch_item_new" />
<java-symbol type="layout" name="input_method_switch_dialog_new" />
<java-symbol type="layout" name="input_method_switch_dialog_title" />
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index ba6f62c..d7f6a29 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -100,14 +100,14 @@
topConsumer.setControl(
new InsetsSourceControl(ID_STATUS_BAR, WindowInsets.Type.statusBars(),
mStatusLeash, true, new Point(0, 0), Insets.of(0, 100, 0, 0)),
- new int[1], new int[1]);
+ new int[1], new int[1], new int[1]);
InsetsSourceConsumer navConsumer = new InsetsSourceConsumer(ID_NAVIGATION_BAR,
WindowInsets.Type.navigationBars(), mInsetsState, mMockController);
navConsumer.setControl(
new InsetsSourceControl(ID_NAVIGATION_BAR, WindowInsets.Type.navigationBars(),
mNavLeash, true, new Point(400, 0), Insets.of(0, 0, 100, 0)),
- new int[1], new int[1]);
+ new int[1], new int[1], new int[1]);
mMockController.setRequestedVisibleTypes(0, WindowInsets.Type.navigationBars());
navConsumer.applyLocalVisibilityOverride();
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index d6d45e8..3a8f7ee 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -117,7 +117,23 @@
mConsumer.setControl(
new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash,
true /* initialVisible */, new Point(), Insets.NONE),
- new int[1], new int[1]);
+ new int[1], new int[1], new int[1]);
+ }
+
+ @Test
+ public void testSetControl_cancelAnimation() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ final InsetsSourceControl newControl = new InsetsSourceControl(mConsumer.getControl());
+
+ // Change the side of the insets hint.
+ newControl.setInsetsHint(Insets.of(0, 0, 0, 100));
+
+ int[] cancelTypes = {0};
+ mConsumer.setControl(newControl, new int[1], new int[1], cancelTypes);
+
+ assertEquals(statusBars(), cancelTypes[0]);
+ });
+
}
@Test
@@ -180,7 +196,7 @@
@Test
public void testRestore() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mConsumer.setControl(null, new int[1], new int[1]);
+ mConsumer.setControl(null, new int[1], new int[1], new int[1]);
mSurfaceParamsApplied = false;
mController.setRequestedVisibleTypes(0 /* visibleTypes */, statusBars());
assertFalse(mSurfaceParamsApplied);
@@ -188,7 +204,7 @@
mConsumer.setControl(
new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash,
true /* initialVisible */, new Point(), Insets.NONE),
- new int[1], hideTypes);
+ new int[1], hideTypes, new int[1]);
assertEquals(statusBars(), hideTypes[0]);
assertFalse(mRemoveSurfaceCalled);
});
@@ -198,7 +214,7 @@
public void testRestore_noAnimation() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.setRequestedVisibleTypes(0 /* visibleTypes */, statusBars());
- mConsumer.setControl(null, new int[1], new int[1]);
+ mConsumer.setControl(null, new int[1], new int[1], new int[1]);
mLeash = new SurfaceControl.Builder(mSession)
.setName("testSurface")
.build();
@@ -207,7 +223,7 @@
mConsumer.setControl(
new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash,
false /* initialVisible */, new Point(), Insets.NONE),
- new int[1], hideTypes);
+ new int[1], hideTypes, new int[1]);
assertTrue(mRemoveSurfaceCalled);
assertEquals(0, hideTypes[0]);
});
@@ -235,7 +251,8 @@
// Initial IME insets source control with its leash.
imeConsumer.setControl(new InsetsSourceControl(ID_IME, ime(), mLeash,
- false /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]);
+ false /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1],
+ new int[1]);
mSurfaceParamsApplied = false;
// Verify when the app requests controlling show IME animation, the IME leash
@@ -244,7 +261,8 @@
null /* interpolator */, null /* cancellationSignal */, null /* listener */);
assertEquals(ANIMATION_TYPE_USER, insetsController.getAnimationType(ime()));
imeConsumer.setControl(new InsetsSourceControl(ID_IME, ime(), mLeash,
- true /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]);
+ true /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1],
+ new int[1]);
assertFalse(mSurfaceParamsApplied);
});
}
diff --git a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
index b74d922..2d5597e 100644
--- a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
+++ b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
@@ -19,9 +19,15 @@
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorTertiary"
- android:viewportHeight="960"
- android:viewportWidth="960">
+ android:viewportHeight="24"
+ android:viewportWidth="24">
<path
android:fillColor="@android:color/system_on_tertiary_container_light"
- android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" />
-</vector>
+ android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4C10,21.1 10.9,22 12,22z"/>
+ <path
+ android:fillColor="@android:color/system_on_tertiary_container_light"
+ android:pathData="M8,17h8v2h-8z"/>
+ <path
+ android:fillColor="@android:color/system_on_tertiary_container_light"
+ android:pathData="M12,2C7.86,2 4.5,5.36 4.5,9.5c0,3.82 2.66,5.86 3.77,6.5h7.46c1.11,-0.64 3.77,-2.68 3.77,-6.5C19.5,5.36 16.14,2 12,2z"/>
+ </vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
index a12a746..473236c 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
@@ -18,7 +18,7 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
- <corners android:radius="30dp" />
+ <corners android:radius="28dp" />
<solid android:color="@android:color/system_tertiary_fixed" />
</shape>
</item>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
index a269b9e..fd75827 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
@@ -26,6 +26,7 @@
android:id="@+id/arrow_icon"
android:layout_width="10dp"
android:layout_height="12dp"
+ android:elevation="2dp"
android:layout_gravity="center_vertical"
android:src="@drawable/desktop_windowing_education_tooltip_left_arrow" />
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
index 09a049c..42f955d 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
@@ -16,16 +16,18 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tooltip_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:background="@drawable/desktop_windowing_education_tooltip_background"
android:orientation="horizontal"
+ android:elevation="2dp"
android:padding="@dimen/desktop_windowing_education_tooltip_padding">
<ImageView
android:id="@+id/tooltip_icon"
android:layout_width="32dp"
android:layout_height="32dp"
+ android:layout_margin="8dp"
android:layout_gravity="center_vertical"
android:src="@drawable/app_handle_education_tooltip_icon" />
@@ -34,9 +36,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
- android:layout_marginStart="2dp"
+ android:layout_marginHorizontal="8dp"
android:lineHeight="20dp"
- android:maxWidth="150dp"
+ android:maxWidth="220dp"
android:textColor="@android:color/system_on_tertiary_container_light"
android:textFontWeight="500"
android:textSize="14sp" />
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
index c73c1da..83d7ef7 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
@@ -25,6 +25,7 @@
android:id="@+id/arrow_icon"
android:layout_width="12dp"
android:layout_height="9dp"
+ android:elevation="2dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/desktop_windowing_education_tooltip_top_arrow" />
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 5ef8432..621e2aa 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -220,13 +220,13 @@
<string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string>
<!-- App handle education tooltip text for tooltip pointing to app handle -->
- <string name="windowing_app_handle_education_tooltip">Tap to open the app menu</string>
+ <string name="windowing_app_handle_education_tooltip">The app menu can be found here</string>
<!-- App handle education tooltip text for tooltip pointing to windowing image button -->
- <string name="windowing_desktop_mode_image_button_education_tooltip">Tap to show multiple apps together</string>
+ <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop view to open multiple apps together</string>
<!-- App handle education tooltip text for tooltip pointing to app chip -->
- <string name="windowing_desktop_mode_exit_education_tooltip">Return to fullscreen from the app menu</string>
+ <string name="windowing_desktop_mode_exit_education_tooltip">Return to full screen anytime from the app menu</string>
<!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] -->
<string name="letterbox_education_dialog_title">See and do more</string>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index 7f1e4a8..61cd1c3 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -170,7 +170,7 @@
SNAP_TO_NONE,
SNAP_TO_START_AND_DISMISS,
SNAP_TO_END_AND_DISMISS,
- SNAP_TO_MINIMIZE
+ SNAP_TO_MINIMIZE,
})
public @interface SnapPosition {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 92535f3..57a59c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -521,7 +521,6 @@
wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED)
transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
-
}
private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
@@ -663,7 +662,6 @@
wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
-
}
/** Moves a task in/out of full immersive state within the desktop. */
@@ -739,7 +737,6 @@
val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
-
}
private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
@@ -851,7 +848,6 @@
val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds)
-
}
@VisibleForTesting
@@ -1246,10 +1242,23 @@
error("Invalid windowing mode: ${callingTask.windowingMode}")
}
}
+ val bounds = when (newTaskWindowingMode) {
+ WINDOWING_MODE_FREEFORM -> {
+ displayController.getDisplayLayout(callingTask.displayId)
+ ?.let { getInitialBounds(it, callingTask) }
+ }
+ WINDOWING_MODE_MULTI_WINDOW -> {
+ Rect()
+ }
+ else -> {
+ error("Invalid windowing mode: $newTaskWindowingMode")
+ }
+ }
return ActivityOptions.makeBasic().apply {
launchWindowingMode = newTaskWindowingMode
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
+ launchBounds = bounds
}
}
@@ -1405,15 +1414,7 @@
} else {
WINDOWING_MODE_FREEFORM
}
- val initialBounds = if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) {
- calculateInitialBounds(displayLayout, taskInfo)
- } else {
- getDefaultDesktopTaskBounds(displayLayout)
- }
-
- if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue()) {
- cascadeWindow(taskInfo, initialBounds, displayLayout)
- }
+ val initialBounds = getInitialBounds(displayLayout, taskInfo)
if (canChangeTaskPosition(taskInfo)) {
wct.setBounds(taskInfo.token, initialBounds)
@@ -1425,6 +1426,22 @@
}
}
+ private fun getInitialBounds(
+ displayLayout: DisplayLayout,
+ taskInfo: RunningTaskInfo
+ ): Rect {
+ val bounds = if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue) {
+ calculateInitialBounds(displayLayout, taskInfo)
+ } else {
+ getDefaultDesktopTaskBounds(displayLayout)
+ }
+
+ if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue) {
+ cascadeWindow(taskInfo, bounds, displayLayout)
+ }
+ return bounds
+ }
+
private fun addMoveToFullscreenChanges(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index dfa2437..5c72cb7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -23,6 +23,7 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT;
@@ -43,14 +44,18 @@
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
+import android.util.Log;
import android.view.DragEvent;
import android.view.SurfaceControl;
+import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.WindowInsets.Type;
+import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.window.WindowContainerToken;
@@ -67,14 +72,22 @@
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiConsumer;
/**
* Coordinates the visible drop targets for the current drag within a single display.
*/
public class DragLayout extends LinearLayout
- implements ViewTreeObserver.OnComputeInternalInsetsListener, DragLayoutProvider {
+ implements ViewTreeObserver.OnComputeInternalInsetsListener, DragLayoutProvider,
+ DragZoneAnimator{
+ static final boolean DEBUG_LAYOUT = false;
// While dragging the status bar is hidden.
private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS
| StatusBarManager.DISABLE_NOTIFICATION_ALERTS
@@ -108,13 +121,19 @@
// The last position that was handled by the drag layout
private final Point mLastPosition = new Point();
+ // Used with enableFlexibleSplit() flag
+ private List<SplitDragPolicy.Target> mTargets;
+ private Map<SplitDragPolicy.Target, DropZoneView> mTargetDropMap = new HashMap<>();
+ private FrameLayout mAnimatingRootLayout;
+ // Used with enableFlexibleSplit() flag
+
@SuppressLint("WrongConstant")
public DragLayout(Context context, SplitScreenController splitScreenController,
IconProvider iconProvider) {
super(context);
mSplitScreenController = splitScreenController;
mIconProvider = iconProvider;
- mPolicy = new SplitDragPolicy(context, splitScreenController);
+ mPolicy = new SplitDragPolicy(context, splitScreenController, this);
mStatusBarManager = context.getSystemService(StatusBarManager.class);
mLastConfiguration.setTo(context.getResources().getConfiguration());
@@ -211,11 +230,26 @@
boolean isLeftRightSplit = mSplitScreenController != null
&& mSplitScreenController.isLeftRightSplit();
if (isLeftRightSplit) {
- mDropZoneView1.setBottomInset(mInsets.bottom);
- mDropZoneView2.setBottomInset(mInsets.bottom);
+ if (enableFlexibleSplit()) {
+ mTargetDropMap.values().forEach(dzv -> dzv.setBottomInset(mInsets.bottom));
+ } else {
+ mDropZoneView1.setBottomInset(mInsets.bottom);
+ mDropZoneView2.setBottomInset(mInsets.bottom);
+ }
} else {
- mDropZoneView1.setBottomInset(0);
- mDropZoneView2.setBottomInset(mInsets.bottom);
+ if (enableFlexibleSplit()) {
+ Collection<DropZoneView> dropViews = mTargetDropMap.values();
+ final DropZoneView[] bottomView = {null};
+ dropViews.forEach(dropZoneView -> {
+ bottomView[0] = dropZoneView;
+ dropZoneView.setBottomInset(0);
+ });
+ // TODO(b/349828130): necessary? maybe with UI polish
+ // bottomView[0].setBottomInset(mInsets.bottom);
+ } else {
+ mDropZoneView1.setBottomInset(0);
+ mDropZoneView2.setBottomInset(mInsets.bottom);
+ }
}
return super.onApplyWindowInsets(insets);
}
@@ -233,17 +267,31 @@
final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0
|| (diff & CONFIG_UI_MODE) != 0;
if (themeChanged) {
- mDropZoneView1.onThemeChange();
- mDropZoneView2.onThemeChange();
+ if (enableFlexibleSplit()) {
+ mTargetDropMap.values().forEach(DropZoneView::onThemeChange);
+ } else {
+ mDropZoneView1.onThemeChange();
+ mDropZoneView2.onThemeChange();
+ }
}
mLastConfiguration.setTo(newConfig);
requestLayout();
}
private void updateContainerMarginsForSingleTask() {
- mDropZoneView1.setContainerMargin(
- mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
- mDropZoneView2.setContainerMargin(0, 0, 0, 0);
+ if (enableFlexibleSplit()) {
+ DropZoneView firstDropZone = mTargetDropMap.values().stream().findFirst().get();
+ mTargetDropMap.values().stream()
+ .filter(dropZoneView -> dropZoneView != firstDropZone)
+ .forEach(dropZoneView -> dropZoneView.setContainerMargin(0, 0, 0, 0));
+ firstDropZone.setContainerMargin(
+ mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin
+ );
+ } else {
+ mDropZoneView1.setContainerMargin(
+ mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
+ mDropZoneView2.setContainerMargin(0, 0, 0, 0);
+ }
}
private void updateContainerMargins(boolean isLeftRightSplit) {
@@ -306,19 +354,35 @@
}
}
} else {
- // We're already in split so get taskInfo from the controller to populate icon / color.
- ActivityManager.RunningTaskInfo topOrLeftTask =
- mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
- ActivityManager.RunningTaskInfo bottomOrRightTask =
- mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
- if (topOrLeftTask != null && bottomOrRightTask != null) {
- Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
- int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
- Drawable bottomOrRightIcon = mIconProvider.getIcon(
- bottomOrRightTask.topActivityInfo);
- int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
- mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
- mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
+ ActivityManager.RunningTaskInfo[] taskInfos = mSplitScreenController.getAllTaskInfos();
+ boolean anyTasksNull = Arrays.stream(taskInfos).anyMatch(Objects::isNull);
+ if (enableFlexibleSplit() && taskInfos != null && !anyTasksNull) {
+ int i = 0;
+ for (DropZoneView v : mTargetDropMap.values()) {
+ if (i >= taskInfos.length) {
+ // TODO(b/349828130) Support once we add 3 StageRoots
+ continue;
+ }
+ ActivityManager.RunningTaskInfo task = taskInfos[i];
+ v.setAppInfo(getResizingBackgroundColor(task),
+ mIconProvider.getIcon(task.topActivityInfo));
+ i++;
+ }
+ } else {
+ // We're already in split so get taskInfo from the controller to populate icon / color.
+ ActivityManager.RunningTaskInfo topOrLeftTask =
+ mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ ActivityManager.RunningTaskInfo bottomOrRightTask =
+ mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ if (topOrLeftTask != null && bottomOrRightTask != null) {
+ Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
+ int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
+ Drawable bottomOrRightIcon = mIconProvider.getIcon(
+ bottomOrRightTask.topActivityInfo);
+ int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
+ mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
+ mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
+ }
}
// Update the dropzones to match existing split sizes
@@ -391,7 +455,14 @@
@NonNull
@Override
public void addDraggingView(ViewGroup rootView) {
- // TODO(b/349828130) We need to separate out view + logic here
+ if (enableFlexibleSplit()) {
+ removeAllViews();
+ mAnimatingRootLayout = new FrameLayout(getContext());
+ addView(mAnimatingRootLayout,
+ new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ ((LayoutParams) mAnimatingRootLayout.getLayoutParams()).weight = 1;
+ }
+
rootView.addView(this, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@@ -409,6 +480,24 @@
// Inset the draw region by a little bit
target.drawRegion.inset(mDisplayMargin, mDisplayMargin);
}
+
+ if (enableFlexibleSplit()) {
+ mTargets = targets;
+ mTargetDropMap.clear();
+ for (int i = 0; i < mTargets.size(); i++) {
+ DropZoneView v = new DropZoneView(getContext());
+ SplitDragPolicy.Target t = mTargets.get(i);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(t.drawRegion.width(),
+ t.drawRegion.height());
+ mAnimatingRootLayout.addView(v, params);
+ v.setTranslationX(t.drawRegion.left);
+ v.setTranslationY(t.drawRegion.top);
+ mTargetDropMap.put(t, v);
+ if (DEBUG_LAYOUT) {
+ v.setDebugIndex(t.index);
+ }
+ }
+ }
}
/**
@@ -433,6 +522,9 @@
if (target == null) {
// Animating to no target
animateSplitContainers(false, null /* animCompleteCallback */);
+ if (enableFlexibleSplit()) {
+ animateHighlight(target);
+ }
} else if (mCurrentTarget == null) {
if (mPolicy.getNumTargets() == 1) {
animateFullscreenContainer(true);
@@ -440,10 +532,14 @@
animateSplitContainers(true, null /* animCompleteCallback */);
animateHighlight(target);
}
- } else if (mCurrentTarget.type != target.type) {
+ } else if (mCurrentTarget.type != target.type || enableFlexibleSplit()) {
// Switching between targets
- mDropZoneView1.animateSwitch();
- mDropZoneView2.animateSwitch();
+ if (enableFlexibleSplit()) {
+ animateHighlight(target);
+ } else {
+ mDropZoneView1.animateSwitch();
+ mDropZoneView2.animateSwitch();
+ }
// Announce for accessibility.
switch (target.type) {
case TYPE_SPLIT_LEFT:
@@ -490,6 +586,9 @@
mDropZoneView2.setForceIgnoreBottomMargin(false);
updateContainerMargins(mIsLeftRightSplit);
mCurrentTarget = null;
+ if (enableFlexibleSplit()) {
+ mAnimatingRootLayout.removeAllViews();
+ }
}
/**
@@ -566,9 +665,20 @@
mStatusBarManager.disable(visible
? HIDE_STATUS_BAR_FLAGS
: DISABLE_NONE);
- mDropZoneView1.setShowingMargin(visible);
- mDropZoneView2.setShowingMargin(visible);
- Animator animator = mDropZoneView1.getAnimator();
+ Animator animator;
+ if (enableFlexibleSplit()) {
+ DropZoneView anyDropZoneView = null;
+ for (DropZoneView dz : mTargetDropMap.values()) {
+ dz.setShowingMargin(visible);
+ anyDropZoneView = dz;
+ }
+ animator = anyDropZoneView != null ? anyDropZoneView.getAnimator() : null;
+ } else {
+ mDropZoneView1.setShowingMargin(visible);
+ mDropZoneView2.setShowingMargin(visible);
+ animator = mDropZoneView1.getAnimator();
+ }
+
if (animCompleteCallback != null) {
if (animator != null) {
animator.addListener(new AnimatorListenerAdapter() {
@@ -584,7 +694,24 @@
}
}
+ @Override
+ public void animateDragTargets(
+ @NonNull List<? extends BiConsumer<SplitDragPolicy.Target, View>> viewsToAnimate) {
+ for (Map.Entry<SplitDragPolicy.Target, DropZoneView> entry : mTargetDropMap.entrySet()) {
+ viewsToAnimate.get(0).accept(entry.getKey(), entry.getValue());
+ }
+ }
+
private void animateHighlight(SplitDragPolicy.Target target) {
+ if (enableFlexibleSplit()) {
+ for (Map.Entry<SplitDragPolicy.Target, DropZoneView> dzv : mTargetDropMap.entrySet()) {
+ // Highlight the view w/ the matching target, unhighlight the rest
+ dzv.getValue().setShowingHighlight(dzv.getKey() == target);
+ }
+ mPolicy.onHoveringOver(target);
+ return;
+ }
+
if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) {
mDropZoneView1.setShowingHighlight(true);
mDropZoneView2.setShowingHighlight(false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragZoneAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragZoneAnimator.kt
new file mode 100644
index 0000000..240465d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragZoneAnimator.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop
+
+import android.view.View
+import java.util.function.BiConsumer
+
+interface DragZoneAnimator {
+ /**
+ * Each consumer will be called for the corresponding DropZoneView.
+ * This must match the number of targets in [.mTargets] otherwise will
+ * throw an [IllegalStateException]
+ */
+ fun animateDragTargets(viewsToAnimate: List<BiConsumer<SplitDragPolicy.Target, View>>)
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt
index 122a105..2bbca48 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt
@@ -47,7 +47,7 @@
/**
* Called when user is hovering Drag object over the given Target
*/
- fun onHoveringOver(target: SplitDragPolicy.Target) {}
+ fun onHoveringOver(target: SplitDragPolicy.Target?) {}
/**
* Called when the user has dropped the provided target (need not be the same target as
* [onHoveringOver])
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
index f9749ec..e503b8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
@@ -20,12 +20,14 @@
import android.animation.Animator;
import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Path;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.Gravity;
@@ -33,6 +35,7 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -83,6 +86,7 @@
private int mTargetBackgroundColor;
private ObjectAnimator mMarginAnimator;
private float mMarginPercent;
+ private TextView mDebugIndex;
// Renders a highlight or neutral transparent color
private ColorDrawable mColorDrawable;
@@ -125,6 +129,22 @@
mMarginView = new MarginView(context);
addView(mMarginView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
+
+ if (DEBUG_LAYOUT) {
+ mDebugIndex = new TextView(context);
+ mDebugIndex.setVisibility(GONE);
+ mDebugIndex.setTextColor(Color.YELLOW);
+ addView(mDebugIndex, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP));
+
+ View borderView = new View(context);
+ addView(borderView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ GradientDrawable border = new GradientDrawable();
+ border.setShape(GradientDrawable.RECTANGLE);
+ border.setStroke(5, Color.RED);
+ borderView.setBackground(border);
+ }
}
public void onThemeChange() {
@@ -236,6 +256,16 @@
}
}
+ @SuppressLint("SetTextI18n")
+ public void setDebugIndex(int index) {
+ if (!DEBUG_LAYOUT) {
+ return;
+ }
+
+ mDebugIndex.setText("Index:\n" + index);
+ mDebugIndex.setVisibility(VISIBLE);
+ }
+
private void animateBackground(int startColor, int endColor) {
if (DEBUG_LAYOUT) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
index 2a19d65..5d22c1e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
@@ -32,16 +32,22 @@
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
+import static com.android.wm.shell.draganddrop.DragLayout.DEBUG_LAYOUT;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_FULLSCREEN;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT;
import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP;
import static com.android.wm.shell.shared.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
@@ -59,6 +65,7 @@
import android.os.UserHandle;
import android.util.Log;
import android.util.Slog;
+import android.view.View;
import android.window.WindowContainerToken;
import androidx.annotation.IntDef;
@@ -69,13 +76,23 @@
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.R;
+import com.android.wm.shell.draganddrop.anim.DropTargetAnimSupplier;
+import com.android.wm.shell.draganddrop.anim.HoverAnimProps;
+import com.android.wm.shell.draganddrop.anim.TwoFiftyFiftyTargetAnimator;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.split.SplitScreenConstants;
import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import kotlin.Pair;
/**
* The policy for handling drag and drop operations to shell.
@@ -89,24 +106,42 @@
private final Starter mFullscreenStarter;
// Used for launching tasks into splitscreen
private final Starter mSplitscreenStarter;
+ private final DragZoneAnimator mDragZoneAnimator;
private final SplitScreenController mSplitScreen;
- private final ArrayList<SplitDragPolicy.Target> mTargets = new ArrayList<>();
+ private ArrayList<SplitDragPolicy.Target> mTargets = new ArrayList<>();
private final RectF mDisallowHitRegion = new RectF();
+ /**
+ * Maps a given SnapPosition to an array where each index of the array represents one
+ * of the targets that are being hovered over, in order (Left to Right, Top to Bottom).
+ * Ex: 4 drop targets when we're in 50/50 split
+ * 2_50_50 => [ [AnimPropsTarget1, AnimPropsTarget2, AnimPropsTarget3, AnimPropsTarget4],
+ * ... // hovering over target 2,
+ * ... // hovering over target 3,
+ * ... // hovering over target 4
+ * ]
+ */
+ private final Map<Integer, List<List<HoverAnimProps>>> mHoverAnimProps = new HashMap();
private InstanceId mLoggerSessionId;
private DragSession mSession;
+ @Nullable
+ private Target mCurrentHoverTarget;
+ /** This variable is a temporary placeholder, will be queried on drag start. */
+ private int mCurrentSnapPosition = -1;
- public SplitDragPolicy(Context context, SplitScreenController splitScreen) {
- this(context, splitScreen, new DefaultStarter(context));
+ public SplitDragPolicy(Context context, SplitScreenController splitScreen,
+ DragZoneAnimator dragZoneAnimator) {
+ this(context, splitScreen, new DefaultStarter(context), dragZoneAnimator);
}
@VisibleForTesting
SplitDragPolicy(Context context, SplitScreenController splitScreen,
- Starter fullscreenStarter) {
+ Starter fullscreenStarter, DragZoneAnimator dragZoneAnimator) {
mContext = context;
mSplitScreen = splitScreen;
mFullscreenStarter = fullscreenStarter;
mSplitscreenStarter = splitScreen;
+ mDragZoneAnimator = dragZoneAnimator;
}
/**
@@ -164,58 +199,123 @@
|| (mSession.runningTaskActType == ACTIVITY_TYPE_STANDARD
&& mSession.runningTaskWinMode == WINDOWING_MODE_FULLSCREEN);
if (allowSplit) {
- // Already split, allow replacing existing split task
- final Rect topOrLeftBounds = new Rect();
- final Rect bottomOrRightBounds = new Rect();
- mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds);
- topOrLeftBounds.intersect(displayRegion);
- bottomOrRightBounds.intersect(displayRegion);
+ if (enableFlexibleSplit()) {
+ // TODO(b/349828130) get this from split screen controller, expose the SnapTarget object
+ // entirely and then pull out the SnapPosition
+ @SplitScreenConstants.SnapPosition int snapPosition = SNAP_TO_2_50_50;
+ final Rect startHitRegion = new Rect();
+ final Rect endHitRegion = new Rect();
+ if (!inSplitScreen) {
+ // Currently in fullscreen, split in half
+ final Rect startBounds = new Rect();
+ final Rect endBounds = new Rect();
+ mSplitScreen.getStageBounds(startBounds, endBounds);
+ startBounds.intersect(displayRegion);
+ endBounds.intersect(displayRegion);
- if (isLeftRightSplit) {
- final Rect leftHitRegion = new Rect();
- final Rect rightHitRegion = new Rect();
+ if (isLeftRightSplit) {
+ displayRegion.splitVertically(startHitRegion, endHitRegion);
+ } else {
+ displayRegion.splitHorizontally(startHitRegion, endHitRegion);
+ }
- // If we have existing split regions use those bounds, otherwise split it 50/50
- if (inSplitScreen) {
- // The bounds of the existing split will have a divider bar, the hit region
- // should include that space. Find the center of the divider bar:
- float centerX = topOrLeftBounds.right + (dividerWidth / 2);
- // Now set the hit regions using that center.
- leftHitRegion.set(displayRegion);
- leftHitRegion.right = (int) centerX;
- rightHitRegion.set(displayRegion);
- rightHitRegion.left = (int) centerX;
+ mTargets.add(new Target(TYPE_SPLIT_LEFT, startHitRegion, startBounds, -1));
+ mTargets.add(new Target(TYPE_SPLIT_RIGHT, endHitRegion, endBounds, -1));
} else {
- displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+ // TODO(b/349828130), move this into init function and/or the insets updating
+ // callback
+ DropTargetAnimSupplier supplier = null;
+ switch (snapPosition) {
+ case SNAP_TO_2_50_50:
+ supplier = new TwoFiftyFiftyTargetAnimator();
+ break;
+ case SplitScreenConstants.SNAP_TO_2_33_66:
+ break;
+ case SplitScreenConstants.SNAP_TO_2_66_33:
+ break;
+ case SplitScreenConstants.SNAP_TO_END_AND_DISMISS:
+ break;
+ case SplitScreenConstants.SNAP_TO_MINIMIZE:
+ break;
+ case SplitScreenConstants.SNAP_TO_NONE:
+ break;
+ case SplitScreenConstants.SNAP_TO_START_AND_DISMISS:
+ break;
+ default:
+ }
+
+ Pair<List<Target>, List<List<HoverAnimProps>>> targetsAnims =
+ supplier.getTargets(mSession.displayLayout,
+ insets, isLeftRightSplit, mContext.getResources());
+ mTargets = new ArrayList<>(targetsAnims.getFirst());
+ mHoverAnimProps.put(SNAP_TO_2_50_50, targetsAnims.getSecond());
+ assert(mTargets.size() == targetsAnims.getSecond().size());
+ if (DEBUG_LAYOUT) {
+ for (List<HoverAnimProps> props : targetsAnims.getSecond()) {
+ StringBuilder sb = new StringBuilder();
+ for (HoverAnimProps hap : props) {
+ sb.append(hap).append("\n");
+ }
+ sb.append("\n");
+ Log.d(TAG, sb.toString());
+ }
+ }
}
-
- mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds));
- mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds));
-
} else {
- final Rect topHitRegion = new Rect();
- final Rect bottomHitRegion = new Rect();
+ // Already split, allow replacing existing split task
+ final Rect topOrLeftBounds = new Rect();
+ final Rect bottomOrRightBounds = new Rect();
+ mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds);
+ topOrLeftBounds.intersect(displayRegion);
+ bottomOrRightBounds.intersect(displayRegion);
- // If we have existing split regions use those bounds, otherwise split it 50/50
- if (inSplitScreen) {
- // The bounds of the existing split will have a divider bar, the hit region
- // should include that space. Find the center of the divider bar:
- float centerX = topOrLeftBounds.bottom + (dividerWidth / 2);
- // Now set the hit regions using that center.
- topHitRegion.set(displayRegion);
- topHitRegion.bottom = (int) centerX;
- bottomHitRegion.set(displayRegion);
- bottomHitRegion.top = (int) centerX;
+ if (isLeftRightSplit) {
+ final Rect leftHitRegion = new Rect();
+ final Rect rightHitRegion = new Rect();
+
+ // If we have existing split regions use those bounds, otherwise split it 50/50
+ if (inSplitScreen) {
+ // The bounds of the existing split will have a divider bar, the hit region
+ // should include that space. Find the center of the divider bar:
+ float centerX = topOrLeftBounds.right + (dividerWidth / 2);
+ // Now set the hit regions using that center.
+ leftHitRegion.set(displayRegion);
+ leftHitRegion.right = (int) centerX;
+ rightHitRegion.set(displayRegion);
+ rightHitRegion.left = (int) centerX;
+ } else {
+ displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+ }
+
+ mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds, -1));
+ mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds,
+ -1));
} else {
- displayRegion.splitHorizontally(topHitRegion, bottomHitRegion);
- }
+ final Rect topHitRegion = new Rect();
+ final Rect bottomHitRegion = new Rect();
- mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds));
- mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds));
+ // If we have existing split regions use those bounds, otherwise split it 50/50
+ if (inSplitScreen) {
+ // The bounds of the existing split will have a divider bar, the hit region
+ // should include that space. Find the center of the divider bar:
+ float centerX = topOrLeftBounds.bottom + (dividerWidth / 2);
+ // Now set the hit regions using that center.
+ topHitRegion.set(displayRegion);
+ topHitRegion.bottom = (int) centerX;
+ bottomHitRegion.set(displayRegion);
+ bottomHitRegion.top = (int) centerX;
+ } else {
+ displayRegion.splitHorizontally(topHitRegion, bottomHitRegion);
+ }
+
+ mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds, -1));
+ mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds,
+ -1));
+ }
}
} else {
// Split-screen not allowed, so only show the fullscreen target
- mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion));
+ mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion, -1));
}
return mTargets;
}
@@ -230,6 +330,22 @@
}
for (int i = mTargets.size() - 1; i >= 0; i--) {
SplitDragPolicy.Target t = mTargets.get(i);
+ if (enableFlexibleSplit() && mCurrentHoverTarget != null) {
+ // If we're in flexible split, the targets themselves animate, so we have to rely
+ // on the view's animated position for subsequent drag coordinates which we also
+ // cache in HoverAnimProps.
+ List<List<HoverAnimProps>> hoverAnimPropTargets =
+ mHoverAnimProps.get(mCurrentSnapPosition);
+ for (HoverAnimProps animProps :
+ hoverAnimPropTargets.get(mCurrentHoverTarget.index)) {
+ if (animProps.getHoverRect() != null &&
+ animProps.getHoverRect().contains(x, y)) {
+ return animProps.getTarget();
+ }
+ }
+
+ }
+
if (t.hitRegion.contains(x, y)) {
return t;
}
@@ -266,6 +382,10 @@
} else {
launchIntent(mSession, starter, position, hideTaskToken);
}
+
+ if (enableFlexibleSplit()) {
+ reset();
+ }
}
/**
@@ -335,6 +455,82 @@
null /* fillIntent */, position, opts, hideTaskToken);
}
+ @Override
+ public void onHoveringOver(Target hoverTarget) {
+ final boolean isLeftRightSplit = mSplitScreen != null && mSplitScreen.isLeftRightSplit();
+ final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible();
+ if (!inSplitScreen) {
+ // no need to animate for entering 50/50 split
+ return;
+ }
+
+ mCurrentHoverTarget = hoverTarget;
+ if (hoverTarget == null) {
+ // Reset to default state
+ BiConsumer<Target, View> biConsumer = new BiConsumer<Target, View>() {
+ @Override
+ public void accept(Target target, View view) {
+ // take into account left/right split
+ Animator transX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
+ target.drawRegion.left);
+ Animator transY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y,
+ target.drawRegion.top);
+ Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, 1);
+ Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, 1);
+ AnimatorSet as = new AnimatorSet();
+ as.play(transX);
+ as.play(transY);
+ as.play(scaleX);
+ as.play(scaleY);
+
+ as.start();
+ }
+ };
+ mDragZoneAnimator.animateDragTargets(List.of(biConsumer));
+ return;
+ }
+
+ // TODO(b/349828130) get this from split controller
+ @SplitScreenConstants.SnapPosition int snapPosition = SNAP_TO_2_50_50;
+ mCurrentSnapPosition = SNAP_TO_2_50_50;
+ List<BiConsumer<Target, View>> animatingConsumers = new ArrayList<>();
+ final List<List<HoverAnimProps>> hoverAnimProps = mHoverAnimProps.get(snapPosition);
+ List<HoverAnimProps> animProps = hoverAnimProps.get(hoverTarget.index);
+ // Expand start and push out the rest to the end
+ BiConsumer<Target, View> biConsumer = new BiConsumer<>() {
+ @Override
+ public void accept(Target target, View view) {
+ if (animProps.isEmpty() || animProps.size() < (target.index + 1)) {
+ return;
+ }
+ HoverAnimProps singleAnimProp = animProps.get(target.index);
+ Animator transX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
+ singleAnimProp.getTransX());
+ Animator transY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y,
+ singleAnimProp.getTransY());
+ Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X,
+ singleAnimProp.getScaleX());
+ Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y,
+ singleAnimProp.getScaleY());
+ AnimatorSet as = new AnimatorSet();
+ as.play(transX);
+ as.play(transY);
+ as.play(scaleX);
+ as.play(scaleY);
+ as.start();
+ }
+ };
+ animatingConsumers.add(biConsumer);
+ mDragZoneAnimator.animateDragTargets(animatingConsumers);
+ }
+
+ private void reset() {
+ mCurrentHoverTarget = null;
+ mCurrentSnapPosition = -1;
+ }
+
+
+
/**
* Interface for actually committing the task launches.
*/
@@ -425,7 +621,7 @@
*/
public static class Target {
static final int TYPE_FULLSCREEN = 0;
- static final int TYPE_SPLIT_LEFT = 1;
+ public static final int TYPE_SPLIT_LEFT = 1;
static final int TYPE_SPLIT_TOP = 2;
static final int TYPE_SPLIT_RIGHT = 3;
static final int TYPE_SPLIT_BOTTOM = 4;
@@ -445,16 +641,23 @@
final Rect hitRegion;
// The approximate visual region for where the task will start
final Rect drawRegion;
+ int index;
- public Target(@Type int t, Rect hit, Rect draw) {
+ /**
+ * @param index 0-indexed, represents which position of drop target this object represents,
+ * 0 to N for left to right, top to bottom
+ */
+ public Target(@Type int t, Rect hit, Rect draw, int index) {
type = t;
hitRegion = hit;
drawRegion = draw;
+ this.index = index;
}
@Override
public String toString() {
- return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion + "}";
+ return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion
+ + " index=" + index + "}";
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/DropTargetAnimSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/DropTargetAnimSupplier.kt
new file mode 100644
index 0000000..bb34613
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/DropTargetAnimSupplier.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop.anim
+
+import android.content.res.Resources
+import android.graphics.Insets
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.draganddrop.SplitDragPolicy
+
+/**
+ * When the user is dragging an icon from Taskbar to add an app into split
+ * screen, we have a set of rules by which we draw and move colored drop
+ * targets around the screen. The rules are provided through this interface.
+ *
+ * Each possible screen layout should have an implementation of this interface.
+ * E.g.
+ * - 50:50 two-app split
+ * - 10:45:45 three-app split
+ * - single app, no split
+ * = three implementations of this interface.
+ */
+interface DropTargetAnimSupplier {
+ /**
+ * Returns a Pair of lists.
+ * First list (length n): Where to draw the n colored drop zones.
+ * Second list (length n): How to animate the drop zones as user hovers around.
+ *
+ * Ex: First list => [A, B, C] // 3 views will be created representing these 3 targets
+ * Second list => [
+ * [A (scaleX=4), B (translateX=20), C (translateX=20)], // hovering over A
+ * [A (translateX=20), B (scaleX=4), C (translateX=20)], // hovering over B
+ * [A (translateX=20), B (translateX=20), C (scaleX=4)], // hovering over C
+ * ]
+ *
+ * All indexes assume 0 to N => left to right when [isLeftRightSplit] is true and top to bottom
+ * when [isLeftRightSplit] is false. Indexing is left to right even in RtL mode.
+ *
+ * All lists should have the SAME number of elements, even if no animations are to be run for
+ * a given target while in a hover state.
+ * It's not that we don't trust you, but we _really_ don't trust you, so this will throw an
+ * exception if lengths are different. Don't ruin it for everyone else...
+ * or do. Idk, you're an adult.
+ */
+ fun getTargets(displayLayout: DisplayLayout, insets: Insets, isLeftRightSplit: Boolean,
+ resources: Resources) :
+ Pair<List<SplitDragPolicy.Target>, List<List<HoverAnimProps>>>
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/HoverAnimProps.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/HoverAnimProps.kt
new file mode 100644
index 0000000..d61caeb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/HoverAnimProps.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop.anim
+
+import android.graphics.Rect
+import com.android.wm.shell.draganddrop.SplitDragPolicy
+
+/**
+ * Contains the animation props to represent a single state of drop targets.
+ * When the user is dragging, we'd be going between different HoverAnimProps
+ */
+data class HoverAnimProps(
+ var target: SplitDragPolicy.Target,
+ val transX: Float,
+ val transY: Float,
+ val scaleX: Float,
+ val scaleY: Float,
+ /**
+ * Pass in null to indicate this target cannot be hovered over for this given animation/
+ * state
+ *
+ * TODO: There's some way we can probably use the existing translation/scaling values
+ * to take [.target]'s hitRect and scale that so we don't have to take in a separate
+ * hoverRect in the CTOR. Have to make sure the pivots match since view's pivot in the
+ * center of the view and rect's pivot at 0, 0 if unspecified.
+ * The two may also not be correlated, but worth investigating
+ *
+ */
+ var hoverRect: Rect?
+) {
+
+ override fun toString(): String {
+ return ("targetId: " + target
+ + " translationX: " + transX
+ + " translationY: " + transY
+ + " scaleX: " + scaleX
+ + " scaleY: " + scaleY
+ + " hoverRect: " + hoverRect)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt
new file mode 100644
index 0000000..9f532f5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop.anim
+
+import android.content.res.Resources
+import android.graphics.Insets
+import android.graphics.Rect
+import com.android.wm.shell.R
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.draganddrop.SplitDragPolicy.Target
+
+/**
+ * Represents Drop Zone targets and animations for when the system is currently in a 2 app 50/50
+ * split.
+ * SnapPosition = 2_50_50
+ *
+ * NOTE: Naming convention for many variables is done as "hXtYZ"
+ * This means that variable is a transformation on the Z property for target index Y while the user
+ * is hovering over target index X
+ * Ex: h1t2scaleX=2 => User is hovering over target index 1, target index 2 should scaleX by 2
+ *
+ * TODO(b/349828130): Everything in this class is temporary, none of this is up to spec.
+ */
+class TwoFiftyFiftyTargetAnimator : DropTargetAnimSupplier {
+ /**
+ * TODO: Could we transpose all the horizontal rects by 90 degrees and have that suffice for
+ * top bottom split?? Hmmm... Doubt it.
+ */
+ override fun getTargets(
+ displayLayout: DisplayLayout,
+ insets: Insets,
+ isLeftRightSplit: Boolean,
+ resources: Resources
+ ): Pair<List<Target>, List<List<HoverAnimProps>>> {
+ val targets : ArrayList<Target> = ArrayList()
+ val w: Int = displayLayout.width()
+ val h: Int = displayLayout.height()
+ val iw = w - insets.left - insets.right
+ val ih = h - insets.top - insets.bottom
+ val l = insets.left
+ val t = insets.top
+ val displayRegion = Rect(l, t, l + iw, t + ih)
+ val fullscreenDrawRegion = Rect(displayRegion)
+ val dividerWidth: Float = resources.getDimensionPixelSize(
+ R.dimen.split_divider_bar_width
+ ).toFloat()
+
+ val farStartBounds = Rect()
+ farStartBounds.set(displayRegion)
+ val startBounds = Rect()
+ startBounds.set(displayRegion)
+ val endBounds = Rect()
+ endBounds.set(displayRegion)
+ val farEndBounds = Rect()
+ farEndBounds.set(displayRegion)
+ val endsPercent = 0.10f
+ val visibleStagePercent = 0.45f
+ val halfDividerWidth = dividerWidth.toInt() / 2
+ val endsWidth = Math.round(displayRegion.width() * endsPercent)
+ val stageWidth = Math.round(displayRegion.width() * visibleStagePercent)
+
+
+ // Place the farStart and farEnds outside of the display, and then
+ // animate them in once the hover starts
+ // | = divider; || = display boundary
+ // farStart || start | end || farEnd
+ farStartBounds.left = -endsWidth
+ farStartBounds.right = 0
+ startBounds.left = farStartBounds.right + dividerWidth.toInt()
+ startBounds.right = startBounds.left + stageWidth
+ endBounds.left = startBounds.right + dividerWidth.toInt()
+ endBounds.right = endBounds.left + stageWidth
+ farEndBounds.left = fullscreenDrawRegion.right
+ farEndBounds.right = farEndBounds.left + endsWidth
+
+
+ // For the hit rect, trim the divider space we've added between the
+ // rects
+ targets.add(
+ Target(
+ Target.TYPE_SPLIT_LEFT,
+ Rect(
+ farStartBounds.left, farStartBounds.top,
+ farStartBounds.right + halfDividerWidth,
+ farStartBounds.bottom
+ ),
+ farStartBounds, 0
+ )
+ )
+ targets.add(
+ Target(
+ Target.TYPE_SPLIT_LEFT,
+ Rect(
+ startBounds.left - halfDividerWidth,
+ startBounds.top,
+ startBounds.right + halfDividerWidth,
+ startBounds.bottom
+ ),
+ startBounds, 1
+ )
+ )
+ targets.add(
+ Target(
+ Target.TYPE_SPLIT_LEFT,
+ Rect(
+ endBounds.left - halfDividerWidth,
+ endBounds.top, endBounds.right, endBounds.bottom
+ ),
+ endBounds, 2
+ )
+ )
+ targets.add(
+ Target(
+ Target.TYPE_SPLIT_LEFT,
+ Rect(
+ farEndBounds.left - halfDividerWidth,
+ farEndBounds.top, farEndBounds.right, farEndBounds.bottom
+ ),
+ farEndBounds, 3
+ )
+ )
+
+
+ // Hovering over target 0,
+ // * increase scaleX of target 0
+ // * decrease scaleX of target 1, 2
+ // * ensure target 3 offscreen
+
+ // bring target 0 in from offscreen and expand
+ val h0t0ScaleX = stageWidth.toFloat() / endsWidth
+ val h0t0TransX: Float = stageWidth / h0t0ScaleX + dividerWidth
+ val h0t0HoverProps = HoverAnimProps(
+ targets.get(0),
+ h0t0TransX, farStartBounds.top.toFloat(), h0t0ScaleX, 1f,
+ Rect(
+ 0, 0, (stageWidth + dividerWidth).toInt(),
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 1 over to the middle/end
+ val h0t1TransX = stageWidth.toFloat()
+ val h0t1ScaleX = 1f
+ val h0t1HoverProps = HoverAnimProps(
+ targets.get(1),
+ h0t1TransX, startBounds.top.toFloat(), h0t1ScaleX, 1f,
+ Rect(
+ stageWidth, 0, (stageWidth + h0t1TransX).toInt(),
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 2 to the very end
+ val h0t2TransX = endBounds.left + stageWidth / 2f
+ val h0t2ScaleX = endsWidth.toFloat() / stageWidth
+ val h0t2HoverProps = HoverAnimProps(
+ targets.get(2),
+ h0t2TransX, endBounds.top.toFloat(), h0t2ScaleX, 1f,
+ Rect(
+ displayRegion.right as Int - endsWidth, 0,
+ displayRegion.right as Int,
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 3 off-screen
+ val h0t3TransX = farEndBounds.right.toFloat()
+ val h0t3ScaleX = 1f
+ val h0t3HoverProps = HoverAnimProps(
+ targets.get(3),
+ h0t3TransX, farEndBounds.top.toFloat(), h0t3ScaleX, 1f,
+ null
+ )
+ val animPropsForHoverTarget0 =
+ listOf(h0t0HoverProps, h0t1HoverProps, h0t2HoverProps, h0t3HoverProps)
+
+
+ // Hovering over target 1,
+ // * Bring in target 0 from offscreen start
+ // * Shift over target 1
+ // * Slightly lower scale of target 2
+ // * Ensure target 4 offscreen
+ // bring target 0 in from offscreen
+ val h1t0TransX = 0f
+ val h1t0ScaleX = 1f
+ val h1t0HoverProps = HoverAnimProps(
+ targets.get(0),
+ h1t0TransX, farStartBounds.top.toFloat(), h1t0ScaleX, 1f,
+ Rect(
+ 0, 0, (farStartBounds.width() + dividerWidth).toInt(),
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 1 over a tiny bit by same amount and make it smaller
+ val h1t1TransX: Float = endsWidth + dividerWidth
+ val h1t1ScaleX = 1f
+ val h1t1HoverProps = HoverAnimProps(
+ targets.get(1),
+ h1t1TransX, startBounds.top.toFloat(), h1t1ScaleX, 1f,
+ Rect(
+ h1t1TransX.toInt(), 0, (h1t1TransX + stageWidth).toInt(),
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 2 to the very end
+ val h1t2TransX = (endBounds.left + farStartBounds.width()).toFloat()
+ val h1t2ScaleX = h1t1ScaleX
+ val h1t2HoverProps = HoverAnimProps(
+ targets.get(2),
+ h1t2TransX, endBounds.top.toFloat(), h1t2ScaleX, 1f,
+ Rect(
+ endBounds.left + farStartBounds.width(),
+ 0,
+ (endBounds.left + farStartBounds.width() + stageWidth),
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 3 off-screen, default laid out is off-screen
+ val h1t3TransX = farEndBounds.right.toFloat()
+ val h1t3ScaleX = 1f
+ val h1t3HoverProps = HoverAnimProps(
+ targets.get(3),
+ h1t3TransX, farEndBounds.top.toFloat(), h1t3ScaleX, 1f,
+ null
+ )
+ val animPropsForHoverTarget1 =
+ listOf(h1t0HoverProps, h1t1HoverProps, h1t2HoverProps, h1t3HoverProps)
+
+
+ // Hovering over target 2,
+ // * Ensure Target 0 offscreen
+ // * Ensure target 1 back to start, slightly smaller scale
+ // * Slightly lower scale of target 2
+ // * Bring target 4 on screen
+ // reset target 0
+ val h2t0TransX = farStartBounds.left.toFloat()
+ val h2t0ScaleX = 1f
+ val h2t0HoverProps = HoverAnimProps(
+ targets.get(0),
+ h2t0TransX, farStartBounds.top.toFloat(), h2t0ScaleX, 1f,
+ null
+ )
+
+
+ // move target 1 over a tiny bit by same amount and make it smaller
+ val h2t1TransX = startBounds.left.toFloat()
+ val h2t1ScaleX = 1f
+ val h2t1HoverProps = HoverAnimProps(
+ targets.get(1),
+ h2t1TransX, startBounds.top.toFloat(), h2t1ScaleX, 1f,
+ Rect(
+ startBounds.left, 0,
+ (startBounds.left + stageWidth),
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 2 to the very end
+ val h2t2TransX = endBounds.left.toFloat()
+ val h2t2ScaleX = h2t1ScaleX
+ val h2t2HoverProps = HoverAnimProps(
+ targets.get(2),
+ h2t2TransX, endBounds.top.toFloat(), h2t2ScaleX, 1f,
+ Rect(
+ (startBounds.right + dividerWidth).toInt(),
+ 0,
+ endBounds.left + stageWidth,
+ farStartBounds.bottom
+ )
+ )
+
+
+ // bring target 3 on-screen
+ val h2t3TransX = (farEndBounds.left - farEndBounds.width()).toFloat()
+ val h2t3ScaleX = 1f
+ val h2t3HoverProps = HoverAnimProps(
+ targets.get(3),
+ h2t3TransX, farEndBounds.top.toFloat(), h2t3ScaleX, 1f,
+ Rect(
+ endBounds.right,
+ 0,
+ displayRegion.right,
+ farStartBounds.bottom
+ )
+ )
+ val animPropsForHoverTarget2 =
+ listOf(h2t0HoverProps, h2t1HoverProps, h2t2HoverProps, h2t3HoverProps)
+
+
+ // Hovering over target 3,
+ // * Ensure Target 0 offscreen
+ // * Ensure target 1 back to start, slightly smaller scale
+ // * Slightly lower scale of target 2
+ // * Bring target 4 on screen and scale up
+ // reset target 0
+ val h3t0TransX = farStartBounds.left.toFloat()
+ val h3t0ScaleX = 1f
+ val h3t0HoverProps = HoverAnimProps(
+ targets.get(0),
+ h3t0TransX, farStartBounds.top.toFloat(), h3t0ScaleX, 1f,
+ null
+ )
+
+
+ // move target 1 over a tiny bit by same amount and make it smaller
+ val h3t1ScaleX = endsWidth.toFloat() / stageWidth
+ val h3t1TransX = 0 - (stageWidth / (1 / h3t1ScaleX))
+ val h3t1HoverProps = HoverAnimProps(
+ targets.get(1),
+ h3t1TransX, startBounds.top.toFloat(), h3t1ScaleX, 1f,
+ Rect(
+ 0, 0,
+ endsWidth,
+ farStartBounds.bottom
+ )
+ )
+
+
+ // move target 2 towards the start
+ val h3t2TransX: Float = endsWidth + dividerWidth
+ val h3t2ScaleX = 1f
+ val h3t2HoverProps = HoverAnimProps(
+ targets.get(2),
+ h3t2TransX, endBounds.top.toFloat(), h3t2ScaleX, 1f,
+ Rect(
+ endsWidth, 0,
+ (endsWidth + stageWidth + dividerWidth).toInt(),
+ farStartBounds.bottom
+ )
+ )
+
+
+ // bring target 3 on-screen and expand
+ val h3t3ScaleX = stageWidth.toFloat() / endsWidth
+ val h3t3TransX = endBounds.right - stageWidth / 2f
+ val h3t3HoverProps = HoverAnimProps(
+ targets.get(3),
+ h3t3TransX, farEndBounds.top.toFloat(), h3t3ScaleX, 1f,
+ Rect(
+ displayRegion.right - stageWidth, 0,
+ displayRegion.right,
+ farStartBounds.bottom
+ )
+ )
+ val animPropsForHoverTarget3 =
+ listOf(h3t0HoverProps, h3t1HoverProps, h3t2HoverProps, h3t3HoverProps)
+
+ return Pair(targets, listOf(animPropsForHoverTarget0, animPropsForHoverTarget1,
+ animPropsForHoverTarget2, animPropsForHoverTarget3))
+
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 87b661d..e77467d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -322,6 +322,22 @@
return mTaskOrganizer.getRunningTaskInfo(taskId);
}
+ /**
+ * @return an Array of RunningTaskInfo's ordered by leftToRight or topTopBottom
+ */
+ @Nullable
+ public ActivityManager.RunningTaskInfo[] getAllTaskInfos() {
+ // TODO(b/349828130) Add the third stage task info and not rely on positions
+ ActivityManager.RunningTaskInfo topLeftTask = getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ ActivityManager.RunningTaskInfo bottomRightTask =
+ getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ if (topLeftTask != null && bottomRightTask != null) {
+ return new ActivityManager.RunningTaskInfo[]{topLeftTask, bottomRightTask};
+ }
+
+ return null;
+ }
+
/** Check task is under split or not by taskId. */
public boolean isTaskInSplitScreen(int taskId) {
return mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED;
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
index 77423af..43ee186 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.flicker.appcompat
import android.platform.test.annotations.Postsubmit
-import android.tools.Rotation
import android.tools.flicker.assertions.FlickerTest
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
@@ -91,9 +90,7 @@
@Parameterized.Parameters(name = "{0}")
@JvmStatic
fun getParams(): Collection<FlickerTest> {
- return LegacyFlickerTestFactory.nonRotationTests(
- supportedRotations = listOf(Rotation.ROTATION_90)
- )
+ return LegacyFlickerTestFactory.nonRotationTests()
}
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
index 46b60499..eb74218 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
@@ -150,7 +150,8 @@
mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false);
mInsets = Insets.of(0, 0, 0, 0);
- mPolicy = spy(new SplitDragPolicy(mContext, mSplitScreenStarter, mFullscreenStarter));
+ mPolicy = spy(new SplitDragPolicy(mContext, mSplitScreenStarter, mFullscreenStarter,
+ mock(DragZoneAnimator.class)));
mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY);
mLaunchableIntentPendingIntent = mock(PendingIntent.class);
when(mLaunchableIntentPendingIntent.getCreatorUserHandle())
diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp
index b4e6b72..56191c0 100644
--- a/libs/hwui/apex/LayoutlibLoader.cpp
+++ b/libs/hwui/apex/LayoutlibLoader.cpp
@@ -205,6 +205,13 @@
jmethodID getPropertyMethod = GetStaticMethodIDOrDie(env, system, "getProperty",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
+ auto formatProperty = (jstring)env->CallStaticObjectMethod(
+ system, getPropertyMethod, env->NewStringUTF("method_binding_format"),
+ env->NewStringUTF(""));
+ const char* methodFormatChars = env->GetStringUTFChars(formatProperty, 0);
+ setJniMethodFormat(string(methodFormatChars));
+ env->ReleaseStringUTFChars(formatProperty, methodFormatChars);
+
// Get the names of classes that need to register their native methods
auto nativesClassesJString = (jstring)env->CallStaticObjectMethod(
system, getPropertyMethod, env->NewStringUTF("graphics_native_classes"),
diff --git a/packages/CrashRecovery/adaptor/Android.bp b/packages/CrashRecovery/adaptor/Android.bp
new file mode 100644
index 0000000..df7c3dd
--- /dev/null
+++ b/packages/CrashRecovery/adaptor/Android.bp
@@ -0,0 +1,12 @@
+filegroup {
+ name: "crashrecovery-platform-adaptor-srcs",
+ srcs: select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
+ "true": [
+ "postModularization/java/**/*.java",
+ ],
+ default: [
+ "preModularization/java/**/*.java",
+ ],
+ }),
+ visibility: ["//frameworks/base:__subpackages__"],
+}
diff --git a/packages/CrashRecovery/adaptor/postModularization/java/com/android/server/crashrecovery/CrashRecoveryAdaptor.java b/packages/CrashRecovery/adaptor/postModularization/java/com/android/server/crashrecovery/CrashRecoveryAdaptor.java
new file mode 100644
index 0000000..b2d798e2
--- /dev/null
+++ b/packages/CrashRecovery/adaptor/postModularization/java/com/android/server/crashrecovery/CrashRecoveryAdaptor.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.crashrecovery;
+
+import android.content.Context;
+
+import com.android.server.PackageWatchdog;
+import com.android.server.SystemServiceManager;
+
+import java.util.List;
+
+/**
+ * This class mediates calls to hidden APIs in CrashRecovery module.
+ * This class is used when the CrashRecovery classes are moved to separate module.
+ *
+ * @hide
+ */
+public class CrashRecoveryAdaptor {
+ private static final String TAG = "CrashRecoveryAdaptor";
+ private static final String CRASHRECOVERY_MODULE_LIFECYCLE_CLASS =
+ "com.android.server.crashrecovery.CrashRecoveryModule$Lifecycle";
+
+ /** Start CrashRecoveryModule LifeCycleService */
+ public static void initializeCrashrecoveryModuleService(
+ SystemServiceManager mSystemServiceManager) {
+ mSystemServiceManager.startService(CRASHRECOVERY_MODULE_LIFECYCLE_CLASS);
+ }
+
+ /** Does Nothing */
+ public static void packageWatchdogNoteBoot(Context mSystemContext) {
+ // do nothing
+ }
+
+ /** Does Nothing */
+ public static void packageWatchdogWriteNow(Context mContext) {
+ // do nothing
+ }
+
+ /** Does Nothing */
+ public static void packageWatchdogOnPackagesReady(PackageWatchdog mPackageWatchdog) {
+ // do nothing
+ }
+
+ /** Does Nothing */
+ public static void rescuePartyRegisterHealthObserver(Context mSystemContext) {
+ // do nothing
+ }
+
+ /** Does Nothing */
+ public static void rescuePartyOnSettingsProviderPublished(Context mContext) {
+ // do nothing
+ }
+
+ /** Does Nothing */
+ public static void rescuePartyResetDeviceConfigForPackages(List<String> packageNames) {
+ // do nothing
+ }
+}
diff --git a/packages/CrashRecovery/adaptor/preModularization/java/com/android/server/crashrecovery/CrashRecoveryAdaptor.java b/packages/CrashRecovery/adaptor/preModularization/java/com/android/server/crashrecovery/CrashRecoveryAdaptor.java
new file mode 100644
index 0000000..74c647c
--- /dev/null
+++ b/packages/CrashRecovery/adaptor/preModularization/java/com/android/server/crashrecovery/CrashRecoveryAdaptor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.crashrecovery;
+
+import android.content.Context;
+
+import com.android.server.PackageWatchdog;
+import com.android.server.RescueParty;
+import com.android.server.SystemServiceManager;
+
+import java.util.List;
+
+/**
+ * This class mediates calls to hidden APIs in CrashRecovery module.
+ * This class is used when CrashRecovery classes are still in platform.
+ *
+ * @hide
+ */
+public class CrashRecoveryAdaptor {
+ private static final String TAG = "CrashRecoveryAdaptor";
+
+ /** Start CrashRecoveryModule LifeCycleService */
+ public static void initializeCrashrecoveryModuleService(
+ SystemServiceManager mSystemServiceManager) {
+ mSystemServiceManager.startService(CrashRecoveryModule.Lifecycle.class);
+ }
+
+ /** Forward calls to PackageWatchdog noteboot */
+ public static void packageWatchdogNoteBoot(Context mSystemContext) {
+ PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ }
+
+ /** Forward calls to PackageWatchdog writeNow */
+ public static void packageWatchdogWriteNow(Context mContext) {
+ PackageWatchdog.getInstance(mContext).writeNow();
+ }
+
+ /** Forward calls to PackageWatchdog OnPackagesReady */
+ public static void packageWatchdogOnPackagesReady(PackageWatchdog mPackageWatchdog) {
+ mPackageWatchdog.onPackagesReady();
+ }
+
+ /** Forward calls to RescueParty RegisterHealthObserver */
+ public static void rescuePartyRegisterHealthObserver(Context mSystemContext) {
+ RescueParty.registerHealthObserver(mSystemContext);
+ }
+
+ /** Forward calls to RescueParty OnSettingsProviderPublished */
+ public static void rescuePartyOnSettingsProviderPublished(Context mContext) {
+ RescueParty.onSettingsProviderPublished(mContext);
+ }
+
+ /** Forward calls to RescueParty ResetDeviceConfigForPackages */
+ public static void rescuePartyResetDeviceConfigForPackages(List<String> packageNames) {
+ RescueParty.resetDeviceConfigForPackages(packageNames);
+ }
+}
diff --git a/packages/CrashRecovery/framework/Android.bp b/packages/CrashRecovery/framework/Android.bp
index 9480327..1be776d 100644
--- a/packages/CrashRecovery/framework/Android.bp
+++ b/packages/CrashRecovery/framework/Android.bp
@@ -1,53 +1,12 @@
-soong_config_module_type {
- name: "platform_filegroup",
- module_type: "filegroup",
- config_namespace: "ANDROID",
- bool_variables: [
- "crashrecovery_files_in_platform",
- ],
- properties: [
- "srcs",
- ],
-}
-
-platform_filegroup {
+filegroup {
name: "framework-crashrecovery-sources",
- soong_config_variables: {
- // if this flag is enabled, then files are part of platform
- crashrecovery_files_in_platform: {
- srcs: [
- "java/**/*.java",
- "java/**/*.aidl",
- ],
- },
- },
- path: "java",
- visibility: ["//frameworks/base:__subpackages__"],
-}
-
-soong_config_module_type {
- name: "module_filegroup",
- module_type: "filegroup",
- config_namespace: "ANDROID",
- bool_variables: [
- "crashrecovery_files_in_module",
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
],
- properties: [
- "srcs",
- ],
-}
-
-module_filegroup {
- name: "framework-crashrecovery-module-sources",
- soong_config_variables: {
- // if this flag is enabled, then files are part of module
- crashrecovery_files_in_module: {
- srcs: [
- "java/**/*.java",
- "java/**/*.aidl",
- ],
- },
- },
path: "java",
- visibility: ["//packages/modules/CrashRecovery/framework"],
+ visibility: [
+ "//frameworks/base:__subpackages__",
+ "//packages/modules/CrashRecovery/framework",
+ ],
}
diff --git a/packages/CrashRecovery/services/Android.bp b/packages/CrashRecovery/services/Android.bp
index 961b41f..1c84402 100644
--- a/packages/CrashRecovery/services/Android.bp
+++ b/packages/CrashRecovery/services/Android.bp
@@ -1,54 +1,18 @@
-soong_config_module_type {
- name: "platform_filegroup",
- module_type: "filegroup",
- config_namespace: "ANDROID",
- bool_variables: [
- "crashrecovery_files_in_platform",
- ],
- properties: [
- "srcs",
- ],
-}
-
-platform_filegroup {
+filegroup {
name: "services-crashrecovery-sources",
- soong_config_variables: {
- // if this flag is enabled, then files are part of platform
- crashrecovery_files_in_platform: {
- srcs: [
- "java/**/*.java",
- "java/**/*.aidl",
- ":statslog-crashrecovery-java-gen",
- ],
- },
- },
+ srcs: [
+ ":crashrecovery-platform-adaptor-srcs",
+ ":statslog-crashrecovery-java-gen",
+ ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
+ "true": [],
+ default: ["platform/java/**/*.java"],
+ }),
visibility: ["//frameworks/base:__subpackages__"],
}
-soong_config_module_type {
- name: "module_filegroup",
- module_type: "filegroup",
- config_namespace: "ANDROID",
- bool_variables: [
- "crashrecovery_files_in_module",
- ],
- properties: [
- "srcs",
- ],
-}
-
-module_filegroup {
+filegroup {
name: "services-crashrecovery-module-sources",
- soong_config_variables: {
- // if this flag is enabled, then files are part of module
- crashrecovery_files_in_module: {
- srcs: [
- "java/**/*.java",
- "java/**/*.aidl",
- ":statslog-crashrecovery-java-gen",
- ],
- },
- },
+ srcs: ["module/java/**/*.java"],
visibility: ["//packages/modules/CrashRecovery/service"],
}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java
new file mode 100644
index 0000000..da9a139
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE;
+import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES;
+import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES;
+import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
+
+import android.Manifest;
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.IBinder;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.watchdog.ExplicitHealthCheckService;
+import android.service.watchdog.IExplicitHealthCheckService;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+
+// TODO(b/120598832): Add tests
+/**
+ * Controls the connections with {@link ExplicitHealthCheckService}.
+ */
+class ExplicitHealthCheckController {
+ private static final String TAG = "ExplicitHealthCheckController";
+ private final Object mLock = new Object();
+ private final Context mContext;
+
+ // Called everytime a package passes the health check, so the watchdog is notified of the
+ // passing check. In practice, should never be null after it has been #setEnabled.
+ // To prevent deadlocks between the controller and watchdog threads, we have
+ // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
+ // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer.
+ @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer;
+ // Called everytime after a successful #syncRequest call, so the watchdog can receive packages
+ // supporting health checks and update its internal state. In practice, should never be null
+ // after it has been #setEnabled.
+ // To prevent deadlocks between the controller and watchdog threads, we have
+ // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
+ // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer.
+ @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer;
+ // Called everytime we need to notify the watchdog to sync requests between itself and the
+ // health check service. In practice, should never be null after it has been #setEnabled.
+ // To prevent deadlocks between the controller and watchdog threads, we have
+ // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
+ // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable.
+ @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable;
+ // Actual binder object to the explicit health check service.
+ @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService;
+ // Connection to the explicit health check service, necessary to unbind.
+ // We should only try to bind if mConnection is null, non-null indicates we
+ // are connected or at least connecting.
+ @GuardedBy("mLock") @Nullable private ServiceConnection mConnection;
+ // Bind state of the explicit health check service.
+ @GuardedBy("mLock") private boolean mEnabled;
+
+ ExplicitHealthCheckController(Context context) {
+ mContext = context;
+ }
+
+ /** Enables or disables explicit health checks. */
+ public void setEnabled(boolean enabled) {
+ synchronized (mLock) {
+ Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled."));
+ mEnabled = enabled;
+ }
+ }
+
+ /**
+ * Sets callbacks to listen to important events from the controller.
+ *
+ * <p> Should be called once at initialization before any other calls to the controller to
+ * ensure a happens-before relationship of the set parameters and visibility on other threads.
+ */
+ public void setCallbacks(Consumer<String> passedConsumer,
+ Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) {
+ synchronized (mLock) {
+ if (mPassedConsumer != null || mSupportedConsumer != null
+ || mNotifySyncRunnable != null) {
+ Slog.wtf(TAG, "Resetting health check controller callbacks");
+ }
+
+ mPassedConsumer = Objects.requireNonNull(passedConsumer);
+ mSupportedConsumer = Objects.requireNonNull(supportedConsumer);
+ mNotifySyncRunnable = Objects.requireNonNull(notifySyncRunnable);
+ }
+ }
+
+ /**
+ * Calls the health check service to request or cancel packages based on
+ * {@code newRequestedPackages}.
+ *
+ * <p> Supported packages in {@code newRequestedPackages} that have not been previously
+ * requested will be requested while supported packages not in {@code newRequestedPackages}
+ * but were previously requested will be cancelled.
+ *
+ * <p> This handles binding and unbinding to the health check service as required.
+ *
+ * <p> Note, calling this may modify {@code newRequestedPackages}.
+ *
+ * <p> Note, this method is not thread safe, all calls should be serialized.
+ */
+ public void syncRequests(Set<String> newRequestedPackages) {
+ boolean enabled;
+ synchronized (mLock) {
+ enabled = mEnabled;
+ }
+
+ if (!enabled) {
+ Slog.i(TAG, "Health checks disabled, no supported packages");
+ // Call outside lock
+ mSupportedConsumer.accept(Collections.emptyList());
+ return;
+ }
+
+ getSupportedPackages(supportedPackageConfigs -> {
+ // Notify the watchdog without lock held
+ mSupportedConsumer.accept(supportedPackageConfigs);
+ getRequestedPackages(previousRequestedPackages -> {
+ synchronized (mLock) {
+ // Hold lock so requests and cancellations are sent atomically.
+ // It is important we don't mix requests from multiple threads.
+
+ Set<String> supportedPackages = new ArraySet<>();
+ for (PackageConfig config : supportedPackageConfigs) {
+ supportedPackages.add(config.getPackageName());
+ }
+ // Note, this may modify newRequestedPackages
+ newRequestedPackages.retainAll(supportedPackages);
+
+ // Cancel packages no longer requested
+ actOnDifference(previousRequestedPackages,
+ newRequestedPackages, p -> cancel(p));
+ // Request packages not yet requested
+ actOnDifference(newRequestedPackages,
+ previousRequestedPackages, p -> request(p));
+
+ if (newRequestedPackages.isEmpty()) {
+ Slog.i(TAG, "No more health check requests, unbinding...");
+ unbindService();
+ return;
+ }
+ }
+ });
+ });
+ }
+
+ private void actOnDifference(Collection<String> collection1, Collection<String> collection2,
+ Consumer<String> action) {
+ Iterator<String> iterator = collection1.iterator();
+ while (iterator.hasNext()) {
+ String packageName = iterator.next();
+ if (!collection2.contains(packageName)) {
+ action.accept(packageName);
+ }
+ }
+ }
+
+ /**
+ * Requests an explicit health check for {@code packageName}.
+ * After this request, the callback registered on {@link #setCallbacks} can receive explicit
+ * health check passed results.
+ */
+ private void request(String packageName) {
+ synchronized (mLock) {
+ if (!prepareServiceLocked("request health check for " + packageName)) {
+ return;
+ }
+
+ Slog.i(TAG, "Requesting health check for package " + packageName);
+ try {
+ mRemoteService.request(packageName);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to request health check for package " + packageName, e);
+ }
+ }
+ }
+
+ /**
+ * Cancels all explicit health checks for {@code packageName}.
+ * After this request, the callback registered on {@link #setCallbacks} can no longer receive
+ * explicit health check passed results.
+ */
+ private void cancel(String packageName) {
+ synchronized (mLock) {
+ if (!prepareServiceLocked("cancel health check for " + packageName)) {
+ return;
+ }
+
+ Slog.i(TAG, "Cancelling health check for package " + packageName);
+ try {
+ mRemoteService.cancel(packageName);
+ } catch (RemoteException e) {
+ // Do nothing, if the service is down, when it comes up, we will sync requests,
+ // if there's some other error, retrying wouldn't fix anyways.
+ Slog.w(TAG, "Failed to cancel health check for package " + packageName, e);
+ }
+ }
+ }
+
+ /**
+ * Returns the packages that we can request explicit health checks for.
+ * The packages will be returned to the {@code consumer}.
+ */
+ private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) {
+ synchronized (mLock) {
+ if (!prepareServiceLocked("get health check supported packages")) {
+ return;
+ }
+
+ Slog.d(TAG, "Getting health check supported packages");
+ try {
+ mRemoteService.getSupportedPackages(new RemoteCallback(result -> {
+ List<PackageConfig> packages =
+ result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, android.service.watchdog.ExplicitHealthCheckService.PackageConfig.class);
+ Slog.i(TAG, "Explicit health check supported packages " + packages);
+ consumer.accept(packages);
+ }));
+ } catch (RemoteException e) {
+ // Request failed, treat as if all observed packages are supported, if any packages
+ // expire during this period, we may incorrectly treat it as failing health checks
+ // even if we don't support health checks for the package.
+ Slog.w(TAG, "Failed to get health check supported packages", e);
+ }
+ }
+ }
+
+ /**
+ * Returns the packages for which health checks are currently in progress.
+ * The packages will be returned to the {@code consumer}.
+ */
+ private void getRequestedPackages(Consumer<List<String>> consumer) {
+ synchronized (mLock) {
+ if (!prepareServiceLocked("get health check requested packages")) {
+ return;
+ }
+
+ Slog.d(TAG, "Getting health check requested packages");
+ try {
+ mRemoteService.getRequestedPackages(new RemoteCallback(result -> {
+ List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES);
+ Slog.i(TAG, "Explicit health check requested packages " + packages);
+ consumer.accept(packages);
+ }));
+ } catch (RemoteException e) {
+ // Request failed, treat as if we haven't requested any packages, if any packages
+ // were actually requested, they will not be cancelled now. May be cancelled later
+ Slog.w(TAG, "Failed to get health check requested packages", e);
+ }
+ }
+ }
+
+ /**
+ * Binds to the explicit health check service if the controller is enabled and
+ * not already bound.
+ */
+ private void bindService() {
+ synchronized (mLock) {
+ if (!mEnabled || mConnection != null || mRemoteService != null) {
+ if (!mEnabled) {
+ Slog.i(TAG, "Not binding to service, service disabled");
+ } else if (mRemoteService != null) {
+ Slog.i(TAG, "Not binding to service, service already connected");
+ } else {
+ Slog.i(TAG, "Not binding to service, service already connecting");
+ }
+ return;
+ }
+ ComponentName component = getServiceComponentNameLocked();
+ if (component == null) {
+ Slog.wtf(TAG, "Explicit health check service not found");
+ return;
+ }
+
+ Intent intent = new Intent();
+ intent.setComponent(component);
+ mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Slog.i(TAG, "Explicit health check service is connected " + name);
+ initState(service);
+ }
+
+ @Override
+ @MainThread
+ public void onServiceDisconnected(ComponentName name) {
+ // Service crashed or process was killed, #onServiceConnected will be called.
+ // Don't need to re-bind.
+ Slog.i(TAG, "Explicit health check service is disconnected " + name);
+ synchronized (mLock) {
+ mRemoteService = null;
+ }
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ // Application hosting service probably got updated
+ // Need to re-bind.
+ Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name);
+ unbindService();
+ bindService();
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ // Should never happen. Service returned null from #onBind.
+ Slog.wtf(TAG, "Explicit health check service binding is null?? " + name);
+ }
+ };
+
+ mContext.bindServiceAsUser(intent, mConnection,
+ Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
+ Slog.i(TAG, "Explicit health check service is bound");
+ }
+ }
+
+ /** Unbinds the explicit health check service. */
+ private void unbindService() {
+ synchronized (mLock) {
+ if (mRemoteService != null) {
+ mContext.unbindService(mConnection);
+ mRemoteService = null;
+ mConnection = null;
+ }
+ Slog.i(TAG, "Explicit health check service is unbound");
+ }
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ServiceInfo getServiceInfoLocked() {
+ final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE);
+ final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA
+ | PackageManager.MATCH_SYSTEM_ONLY);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ Slog.w(TAG, "No valid components found.");
+ return null;
+ }
+ return resolveInfo.serviceInfo;
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ComponentName getServiceComponentNameLocked() {
+ final ServiceInfo serviceInfo = getServiceInfoLocked();
+ if (serviceInfo == null) {
+ return null;
+ }
+
+ final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE
+ .equals(serviceInfo.permission)) {
+ Slog.w(TAG, name.flattenToShortString() + " does not require permission "
+ + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE);
+ return null;
+ }
+ return name;
+ }
+
+ private void initState(IBinder service) {
+ synchronized (mLock) {
+ if (!mEnabled) {
+ Slog.w(TAG, "Attempting to connect disabled service?? Unbinding...");
+ // Very unlikely, but we disabled the service after binding but before we connected
+ unbindService();
+ return;
+ }
+ mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service);
+ try {
+ mRemoteService.setCallback(new RemoteCallback(result -> {
+ String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE);
+ if (!TextUtils.isEmpty(packageName)) {
+ if (mPassedConsumer == null) {
+ Slog.wtf(TAG, "Health check passed for package " + packageName
+ + "but no consumer registered.");
+ } else {
+ // Call without lock held
+ mPassedConsumer.accept(packageName);
+ }
+ } else {
+ Slog.wtf(TAG, "Empty package passed explicit health check?");
+ }
+ }));
+ Slog.i(TAG, "Service initialized, syncing requests");
+ } catch (RemoteException e) {
+ Slog.wtf(TAG, "Could not setCallback on explicit health check service");
+ }
+ }
+ // Calling outside lock
+ mNotifySyncRunnable.run();
+ }
+
+ /**
+ * Prepares the health check service to receive requests.
+ *
+ * @return {@code true} if it is ready and we can proceed with a request,
+ * {@code false} otherwise. If it is not ready, and the service is enabled,
+ * we will bind and the request should be automatically attempted later.
+ */
+ @GuardedBy("mLock")
+ private boolean prepareServiceLocked(String action) {
+ if (mRemoteService != null && mEnabled) {
+ return true;
+ }
+ Slog.i(TAG, "Service not ready to " + action
+ + (mEnabled ? ". Binding..." : ". Disabled"));
+ if (mEnabled) {
+ bindService();
+ }
+ return false;
+ }
+}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java
new file mode 100644
index 0000000..9a8261c
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java
@@ -0,0 +1,2094 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.content.Intent.ACTION_REBOOT;
+import static android.content.Intent.ACTION_SHUTDOWN;
+import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
+
+import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecoveryEvents;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.provider.DeviceConfig;
+import android.sysprop.CrashRecoveryProperties;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.EventLog;
+import android.util.IndentingPrintWriter;
+import android.util.LongArrayQueue;
+import android.util.Slog;
+import android.util.Xml;
+import android.util.XmlUtils;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.BackgroundThread;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Monitors the health of packages on the system and notifies interested observers when packages
+ * fail. On failure, the registered observer with the least user impacting mitigation will
+ * be notified.
+ * @hide
+ */
+public class PackageWatchdog {
+ private static final String TAG = "PackageWatchdog";
+
+ static final String PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS =
+ "watchdog_trigger_failure_duration_millis";
+ static final String PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT =
+ "watchdog_trigger_failure_count";
+ static final String PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED =
+ "watchdog_explicit_health_check_enabled";
+
+ // TODO: make the following values configurable via DeviceConfig
+ private static final long NATIVE_CRASH_POLLING_INTERVAL_MILLIS =
+ TimeUnit.SECONDS.toMillis(30);
+ private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10;
+
+
+ /** Reason for package failure could not be determined. */
+ public static final int FAILURE_REASON_UNKNOWN = 0;
+
+ /** The package had a native crash. */
+ public static final int FAILURE_REASON_NATIVE_CRASH = 1;
+
+ /** The package failed an explicit health check. */
+ public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2;
+
+ /** The app crashed. */
+ public static final int FAILURE_REASON_APP_CRASH = 3;
+
+ /** The app was not responding. */
+ public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4;
+
+ /** The device was boot looping. */
+ public static final int FAILURE_REASON_BOOT_LOOP = 5;
+
+ /** @hide */
+ @IntDef(prefix = { "FAILURE_REASON_" }, value = {
+ FAILURE_REASON_UNKNOWN,
+ FAILURE_REASON_NATIVE_CRASH,
+ FAILURE_REASON_EXPLICIT_HEALTH_CHECK,
+ FAILURE_REASON_APP_CRASH,
+ FAILURE_REASON_APP_NOT_RESPONDING,
+ FAILURE_REASON_BOOT_LOOP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FailureReasons {}
+
+ // Duration to count package failures before it resets to 0
+ @VisibleForTesting
+ static final int DEFAULT_TRIGGER_FAILURE_DURATION_MS =
+ (int) TimeUnit.MINUTES.toMillis(1);
+ // Number of package failures within the duration above before we notify observers
+ @VisibleForTesting
+ static final int DEFAULT_TRIGGER_FAILURE_COUNT = 5;
+ @VisibleForTesting
+ static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2);
+ // Sliding window for tracking how many mitigation calls were made for a package.
+ @VisibleForTesting
+ static final long DEFAULT_DEESCALATION_WINDOW_MS = TimeUnit.HOURS.toMillis(1);
+ // Whether explicit health checks are enabled or not
+ private static final boolean DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED = true;
+
+ @VisibleForTesting
+ static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5;
+
+ static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10);
+
+ // Time needed to apply mitigation
+ private static final String MITIGATION_WINDOW_MS =
+ "persist.device_config.configuration.mitigation_window_ms";
+ @VisibleForTesting
+ static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5);
+
+ // Threshold level at which or above user might experience significant disruption.
+ private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
+ "persist.device_config.configuration.major_user_impact_level_threshold";
+ private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
+
+ // Comma separated list of all packages exempt from user impact level threshold. If a package
+ // in the list is crash looping, all the mitigations including factory reset will be performed.
+ private static final String PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD =
+ "persist.device_config.configuration.packages_exempt_from_impact_level_threshold";
+
+ // Comma separated list of default packages exempt from user impact level threshold.
+ private static final String DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD =
+ "com.android.systemui";
+
+ private long mNumberOfNativeCrashPollsRemaining;
+
+ private static final int DB_VERSION = 1;
+ private static final String TAG_PACKAGE_WATCHDOG = "package-watchdog";
+ private static final String TAG_PACKAGE = "package";
+ private static final String TAG_OBSERVER = "observer";
+ private static final String ATTR_VERSION = "version";
+ private static final String ATTR_NAME = "name";
+ private static final String ATTR_DURATION = "duration";
+ private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration";
+ private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check";
+ private static final String ATTR_MITIGATION_CALLS = "mitigation-calls";
+ private static final String ATTR_MITIGATION_COUNT = "mitigation-count";
+
+ // A file containing information about the current mitigation count in the case of a boot loop.
+ // This allows boot loop information to persist in the case of an fs-checkpoint being
+ // aborted.
+ private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt";
+
+ /**
+ * EventLog tags used when logging into the event log. Note the values must be sync with
+ * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct
+ * name translation.
+ */
+ private static final int LOG_TAG_RESCUE_NOTE = 2900;
+
+ private static final Object sPackageWatchdogLock = new Object();
+ @GuardedBy("sPackageWatchdogLock")
+ private static PackageWatchdog sPackageWatchdog;
+
+ private final Object mLock = new Object();
+ // System server context
+ private final Context mContext;
+ // Handler to run short running tasks
+ private final Handler mShortTaskHandler;
+ // Handler for processing IO and long running tasks
+ private final Handler mLongTaskHandler;
+ // Contains (observer-name -> observer-handle) that have ever been registered from
+ // previous boots. Observers with all packages expired are periodically pruned.
+ // It is saved to disk on system shutdown and repouplated on startup so it survives reboots.
+ @GuardedBy("mLock")
+ private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>();
+ // File containing the XML data of monitored packages /data/system/package-watchdog.xml
+ private final AtomicFile mPolicyFile;
+ private final ExplicitHealthCheckController mHealthCheckController;
+ private final Runnable mSyncRequests = this::syncRequests;
+ private final Runnable mSyncStateWithScheduledReason = this::syncStateWithScheduledReason;
+ private final Runnable mSaveToFile = this::saveToFile;
+ private final SystemClock mSystemClock;
+ private final BootThreshold mBootThreshold;
+ private final DeviceConfig.OnPropertiesChangedListener
+ mOnPropertyChangedListener = this::onPropertyChanged;
+
+ private final Set<String> mPackagesExemptFromImpactLevelThreshold = new ArraySet<>();
+
+ // The set of packages that have been synced with the ExplicitHealthCheckController
+ @GuardedBy("mLock")
+ private Set<String> mRequestedHealthCheckPackages = new ArraySet<>();
+ @GuardedBy("mLock")
+ private boolean mIsPackagesReady;
+ // Flag to control whether explicit health checks are supported or not
+ @GuardedBy("mLock")
+ private boolean mIsHealthCheckEnabled = DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED;
+ @GuardedBy("mLock")
+ private int mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS;
+ @GuardedBy("mLock")
+ private int mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT;
+ // SystemClock#uptimeMillis when we last executed #syncState
+ // 0 if no prune is scheduled.
+ @GuardedBy("mLock")
+ private long mUptimeAtLastStateSync;
+ // If true, sync explicit health check packages with the ExplicitHealthCheckController.
+ @GuardedBy("mLock")
+ private boolean mSyncRequired = false;
+
+ @GuardedBy("mLock")
+ private long mLastMitigation = -1000000;
+
+ @FunctionalInterface
+ @VisibleForTesting
+ interface SystemClock {
+ long uptimeMillis();
+ }
+
+ private PackageWatchdog(Context context) {
+ // Needs to be constructed inline
+ this(context, new AtomicFile(
+ new File(new File(Environment.getDataDirectory(), "system"),
+ "package-watchdog.xml")),
+ new Handler(Looper.myLooper()), BackgroundThread.getHandler(),
+ new ExplicitHealthCheckController(context),
+ android.os.SystemClock::uptimeMillis);
+ }
+
+ /**
+ * Creates a PackageWatchdog that allows injecting dependencies.
+ */
+ @VisibleForTesting
+ PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler,
+ Handler longTaskHandler, ExplicitHealthCheckController controller,
+ SystemClock clock) {
+ mContext = context;
+ mPolicyFile = policyFile;
+ mShortTaskHandler = shortTaskHandler;
+ mLongTaskHandler = longTaskHandler;
+ mHealthCheckController = controller;
+ mSystemClock = clock;
+ mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS;
+ mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
+
+ loadFromFile();
+ sPackageWatchdog = this;
+ }
+
+ /** Creates or gets singleton instance of PackageWatchdog. */
+ public static @NonNull PackageWatchdog getInstance(@NonNull Context context) {
+ synchronized (sPackageWatchdogLock) {
+ if (sPackageWatchdog == null) {
+ new PackageWatchdog(context);
+ }
+ return sPackageWatchdog;
+ }
+ }
+
+ /**
+ * Called during boot to notify when packages are ready on the device so we can start
+ * binding.
+ * @hide
+ */
+ public void onPackagesReady() {
+ synchronized (mLock) {
+ mIsPackagesReady = true;
+ mHealthCheckController.setCallbacks(packageName -> onHealthCheckPassed(packageName),
+ packages -> onSupportedPackages(packages),
+ this::onSyncRequestNotified);
+ setPropertyChangedListenerLocked();
+ updateConfigs();
+ }
+ }
+
+ /**
+ * Registers {@code observer} to listen for package failures. Add a new ObserverInternal for
+ * this observer if it does not already exist.
+ *
+ * <p>Observers are expected to call this on boot. It does not specify any packages but
+ * it will resume observing any packages requested from a previous boot.
+ * @hide
+ */
+ public void registerHealthObserver(PackageHealthObserver observer) {
+ synchronized (mLock) {
+ ObserverInternal internalObserver = mAllObservers.get(observer.getUniqueIdentifier());
+ if (internalObserver != null) {
+ internalObserver.registeredObserver = observer;
+ } else {
+ internalObserver = new ObserverInternal(observer.getUniqueIdentifier(),
+ new ArrayList<>());
+ internalObserver.registeredObserver = observer;
+ mAllObservers.put(observer.getUniqueIdentifier(), internalObserver);
+ syncState("added new observer");
+ }
+ }
+ }
+
+ /**
+ * Starts observing the health of the {@code packages} for {@code observer} and notifies
+ * {@code observer} of any package failures within the monitoring duration.
+ *
+ * <p>If monitoring a package supporting explicit health check, at the end of the monitoring
+ * duration if {@link #onHealthCheckPassed} was never called,
+ * {@link PackageHealthObserver#execute} will be called as if the package failed.
+ *
+ * <p>If {@code observer} is already monitoring a package in {@code packageNames},
+ * the monitoring window of that package will be reset to {@code durationMs} and the health
+ * check state will be reset to a default depending on if the package is contained in
+ * {@link mPackagesWithExplicitHealthCheckEnabled}.
+ *
+ * <p>If {@code packageNames} is empty, this will be a no-op.
+ *
+ * <p>If {@code durationMs} is less than 1, a default monitoring duration
+ * {@link #DEFAULT_OBSERVING_DURATION_MS} will be used.
+ * @hide
+ */
+ public void startObservingHealth(PackageHealthObserver observer, List<String> packageNames,
+ long durationMs) {
+ if (packageNames.isEmpty()) {
+ Slog.wtf(TAG, "No packages to observe, " + observer.getUniqueIdentifier());
+ return;
+ }
+ if (durationMs < 1) {
+ Slog.wtf(TAG, "Invalid duration " + durationMs + "ms for observer "
+ + observer.getUniqueIdentifier() + ". Not observing packages " + packageNames);
+ durationMs = DEFAULT_OBSERVING_DURATION_MS;
+ }
+
+ List<MonitoredPackage> packages = new ArrayList<>();
+ for (int i = 0; i < packageNames.size(); i++) {
+ // Health checks not available yet so health check state will start INACTIVE
+ MonitoredPackage pkg = newMonitoredPackage(packageNames.get(i), durationMs, false);
+ if (pkg != null) {
+ packages.add(pkg);
+ } else {
+ Slog.w(TAG, "Failed to create MonitoredPackage for pkg=" + packageNames.get(i));
+ }
+ }
+
+ if (packages.isEmpty()) {
+ return;
+ }
+
+ // Sync before we add the new packages to the observers. This will #pruneObservers,
+ // causing any elapsed time to be deducted from all existing packages before we add new
+ // packages. This maintains the invariant that the elapsed time for ALL (new and existing)
+ // packages is the same.
+ mLongTaskHandler.post(() -> {
+ syncState("observing new packages");
+
+ synchronized (mLock) {
+ ObserverInternal oldObserver = mAllObservers.get(observer.getUniqueIdentifier());
+ if (oldObserver == null) {
+ Slog.d(TAG, observer.getUniqueIdentifier() + " started monitoring health "
+ + "of packages " + packageNames);
+ mAllObservers.put(observer.getUniqueIdentifier(),
+ new ObserverInternal(observer.getUniqueIdentifier(), packages));
+ } else {
+ Slog.d(TAG, observer.getUniqueIdentifier() + " added the following "
+ + "packages to monitor " + packageNames);
+ oldObserver.updatePackagesLocked(packages);
+ }
+ }
+
+ // Register observer in case not already registered
+ registerHealthObserver(observer);
+
+ // Sync after we add the new packages to the observers. We may have received packges
+ // requiring an earlier schedule than we are currently scheduled for.
+ syncState("updated observers");
+ });
+
+ }
+
+ /**
+ * Unregisters {@code observer} from listening to package failure.
+ * Additionally, this stops observing any packages that may have previously been observed
+ * even from a previous boot.
+ * @hide
+ */
+ public void unregisterHealthObserver(PackageHealthObserver observer) {
+ mLongTaskHandler.post(() -> {
+ synchronized (mLock) {
+ mAllObservers.remove(observer.getUniqueIdentifier());
+ }
+ syncState("unregistering observer: " + observer.getUniqueIdentifier());
+ });
+ }
+
+ /**
+ * Called when a process fails due to a crash, ANR or explicit health check.
+ *
+ * <p>For each package contained in the process, one registered observer with the least user
+ * impact will be notified for mitigation.
+ *
+ * <p>This method could be called frequently if there is a severe problem on the device.
+ */
+ public void onPackageFailure(@NonNull List<VersionedPackage> packages,
+ @FailureReasons int failureReason) {
+ if (packages == null) {
+ Slog.w(TAG, "Could not resolve a list of failing packages");
+ return;
+ }
+ synchronized (mLock) {
+ final long now = mSystemClock.uptimeMillis();
+ if (Flags.recoverabilityDetection()) {
+ if (now >= mLastMitigation
+ && (now - mLastMitigation) < getMitigationWindowMs()) {
+ Slog.i(TAG, "Skipping onPackageFailure mitigation");
+ return;
+ }
+ }
+ }
+ mLongTaskHandler.post(() -> {
+ synchronized (mLock) {
+ if (mAllObservers.isEmpty()) {
+ return;
+ }
+ boolean requiresImmediateAction = (failureReason == FAILURE_REASON_NATIVE_CRASH
+ || failureReason == FAILURE_REASON_EXPLICIT_HEALTH_CHECK);
+ if (requiresImmediateAction) {
+ handleFailureImmediately(packages, failureReason);
+ } else {
+ for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
+ VersionedPackage versionedPackage = packages.get(pIndex);
+ // Observer that will receive failure for versionedPackage
+ PackageHealthObserver currentObserverToNotify = null;
+ int currentObserverImpact = Integer.MAX_VALUE;
+ MonitoredPackage currentMonitoredPackage = null;
+
+ // Find observer with least user impact
+ for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
+ ObserverInternal observer = mAllObservers.valueAt(oIndex);
+ PackageHealthObserver registeredObserver = observer.registeredObserver;
+ if (registeredObserver != null
+ && observer.onPackageFailureLocked(
+ versionedPackage.getPackageName())) {
+ MonitoredPackage p = observer.getMonitoredPackage(
+ versionedPackage.getPackageName());
+ int mitigationCount = 1;
+ if (p != null) {
+ mitigationCount = p.getMitigationCountLocked() + 1;
+ }
+ int impact = registeredObserver.onHealthCheckFailed(
+ versionedPackage, failureReason, mitigationCount);
+ if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
+ && impact < currentObserverImpact) {
+ currentObserverToNotify = registeredObserver;
+ currentObserverImpact = impact;
+ currentMonitoredPackage = p;
+ }
+ }
+ }
+
+ // Execute action with least user impact
+ if (currentObserverToNotify != null) {
+ int mitigationCount = 1;
+ if (currentMonitoredPackage != null) {
+ currentMonitoredPackage.noteMitigationCallLocked();
+ mitigationCount =
+ currentMonitoredPackage.getMitigationCountLocked();
+ }
+ if (Flags.recoverabilityDetection()) {
+ maybeExecute(currentObserverToNotify, versionedPackage,
+ failureReason, currentObserverImpact, mitigationCount);
+ } else {
+ currentObserverToNotify.execute(versionedPackage,
+ failureReason, mitigationCount);
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * For native crashes or explicit health check failures, call directly into each observer to
+ * mitigate the error without going through failure threshold logic.
+ */
+ private void handleFailureImmediately(List<VersionedPackage> packages,
+ @FailureReasons int failureReason) {
+ VersionedPackage failingPackage = packages.size() > 0 ? packages.get(0) : null;
+ PackageHealthObserver currentObserverToNotify = null;
+ int currentObserverImpact = Integer.MAX_VALUE;
+ for (ObserverInternal observer: mAllObservers.values()) {
+ PackageHealthObserver registeredObserver = observer.registeredObserver;
+ if (registeredObserver != null) {
+ int impact = registeredObserver.onHealthCheckFailed(
+ failingPackage, failureReason, 1);
+ if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
+ && impact < currentObserverImpact) {
+ currentObserverToNotify = registeredObserver;
+ currentObserverImpact = impact;
+ }
+ }
+ }
+ if (currentObserverToNotify != null) {
+ if (Flags.recoverabilityDetection()) {
+ maybeExecute(currentObserverToNotify, failingPackage, failureReason,
+ currentObserverImpact, /*mitigationCount=*/ 1);
+ } else {
+ currentObserverToNotify.execute(failingPackage, failureReason, 1);
+ }
+ }
+ }
+
+ private void maybeExecute(PackageHealthObserver currentObserverToNotify,
+ VersionedPackage versionedPackage,
+ @FailureReasons int failureReason,
+ int currentObserverImpact,
+ int mitigationCount) {
+ if (allowMitigations(currentObserverImpact, versionedPackage)) {
+ synchronized (mLock) {
+ mLastMitigation = mSystemClock.uptimeMillis();
+ }
+ currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount);
+ }
+ }
+
+ private boolean allowMitigations(int currentObserverImpact,
+ VersionedPackage versionedPackage) {
+ return currentObserverImpact < getUserImpactLevelLimit()
+ || getPackagesExemptFromImpactLevelThreshold().contains(
+ versionedPackage.getPackageName());
+ }
+
+ private long getMitigationWindowMs() {
+ return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS);
+ }
+
+
+ /**
+ * Called when the system server boots. If the system server is detected to be in a boot loop,
+ * query each observer and perform the mitigation action with the lowest user impact.
+ *
+ * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots
+ * are not counted in bootloop.
+ * @hide
+ */
+ @SuppressWarnings("GuardedBy")
+ public void noteBoot() {
+ synchronized (mLock) {
+ // if boot count has reached threshold, start mitigation.
+ // We wait until threshold number of restarts only for the first time. Perform
+ // mitigations for every restart after that.
+ boolean mitigate = mBootThreshold.incrementAndTest();
+ if (mitigate) {
+ if (!Flags.recoverabilityDetection()) {
+ mBootThreshold.reset();
+ }
+ int mitigationCount = mBootThreshold.getMitigationCount() + 1;
+ PackageHealthObserver currentObserverToNotify = null;
+ ObserverInternal currentObserverInternal = null;
+ int currentObserverImpact = Integer.MAX_VALUE;
+ for (int i = 0; i < mAllObservers.size(); i++) {
+ final ObserverInternal observer = mAllObservers.valueAt(i);
+ PackageHealthObserver registeredObserver = observer.registeredObserver;
+ if (registeredObserver != null) {
+ int impact = Flags.recoverabilityDetection()
+ ? registeredObserver.onBootLoop(
+ observer.getBootMitigationCount() + 1)
+ : registeredObserver.onBootLoop(mitigationCount);
+ if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
+ && impact < currentObserverImpact) {
+ currentObserverToNotify = registeredObserver;
+ currentObserverInternal = observer;
+ currentObserverImpact = impact;
+ }
+ }
+ }
+ if (currentObserverToNotify != null) {
+ if (Flags.recoverabilityDetection()) {
+ int currentObserverMitigationCount =
+ currentObserverInternal.getBootMitigationCount() + 1;
+ currentObserverInternal.setBootMitigationCount(
+ currentObserverMitigationCount);
+ saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
+ currentObserverToNotify.executeBootLoopMitigation(
+ currentObserverMitigationCount);
+ } else {
+ mBootThreshold.setMitigationCount(mitigationCount);
+ mBootThreshold.saveMitigationCountToMetadata();
+ currentObserverToNotify.executeBootLoopMitigation(mitigationCount);
+ }
+ }
+ }
+ }
+ }
+
+ // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also
+ // avoid holding lock?
+ // This currently adds about 7ms extra to shutdown thread
+ /** @hide Writes the package information to file during shutdown. */
+ public void writeNow() {
+ synchronized (mLock) {
+ // Must only run synchronous tasks as this runs on the ShutdownThread and no other
+ // thread is guaranteed to run during shutdown.
+ if (!mAllObservers.isEmpty()) {
+ mLongTaskHandler.removeCallbacks(mSaveToFile);
+ pruneObserversLocked();
+ saveToFile();
+ Slog.i(TAG, "Last write to update package durations");
+ }
+ }
+ }
+
+ /**
+ * Enables or disables explicit health checks.
+ * <p> If explicit health checks are enabled, the health check service is started.
+ * <p> If explicit health checks are disabled, pending explicit health check requests are
+ * passed and the health check service is stopped.
+ */
+ private void setExplicitHealthCheckEnabled(boolean enabled) {
+ synchronized (mLock) {
+ mIsHealthCheckEnabled = enabled;
+ mHealthCheckController.setEnabled(enabled);
+ mSyncRequired = true;
+ // Prune to update internal state whenever health check is enabled/disabled
+ syncState("health check state " + (enabled ? "enabled" : "disabled"));
+ }
+ }
+
+ /**
+ * This method should be only called on mShortTaskHandler, since it modifies
+ * {@link #mNumberOfNativeCrashPollsRemaining}.
+ */
+ private void checkAndMitigateNativeCrashes() {
+ mNumberOfNativeCrashPollsRemaining--;
+ // Check if native watchdog reported a crash
+ if ("1".equals(SystemProperties.get("sys.init.updatable_crashing"))) {
+ // We rollback all available low impact rollbacks when crash is unattributable
+ onPackageFailure(Collections.EMPTY_LIST, FAILURE_REASON_NATIVE_CRASH);
+ // we stop polling after an attempt to execute rollback, regardless of whether the
+ // attempt succeeds or not
+ } else {
+ if (mNumberOfNativeCrashPollsRemaining > 0) {
+ mShortTaskHandler.postDelayed(() -> checkAndMitigateNativeCrashes(),
+ NATIVE_CRASH_POLLING_INTERVAL_MILLIS);
+ }
+ }
+ }
+
+ /**
+ * Since this method can eventually trigger a rollback, it should be called
+ * only once boot has completed {@code onBootCompleted} and not earlier, because the install
+ * session must be entirely completed before we try to rollback.
+ * @hide
+ */
+ public void scheduleCheckAndMitigateNativeCrashes() {
+ Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check "
+ + "and mitigate native crashes");
+ mShortTaskHandler.post(()->checkAndMitigateNativeCrashes());
+ }
+
+ private int getUserImpactLevelLimit() {
+ return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD,
+ DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD);
+ }
+
+ private Set<String> getPackagesExemptFromImpactLevelThreshold() {
+ if (mPackagesExemptFromImpactLevelThreshold.isEmpty()) {
+ String packageNames = SystemProperties.get(PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD,
+ DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD);
+ return Set.of(packageNames.split("\\s*,\\s*"));
+ }
+ return mPackagesExemptFromImpactLevelThreshold;
+ }
+
+ /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}.
+ * @hide
+ */
+ @Retention(SOURCE)
+ @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_10,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_40,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_50,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_71,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_75,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_80,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_90,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_100})
+ public @interface PackageHealthObserverImpact {
+ /** No action to take. */
+ int USER_IMPACT_LEVEL_0 = 0;
+ /* Action has low user impact, user of a device will barely notice. */
+ int USER_IMPACT_LEVEL_10 = 10;
+ /* Actions having medium user impact, user of a device will likely notice. */
+ int USER_IMPACT_LEVEL_20 = 20;
+ int USER_IMPACT_LEVEL_30 = 30;
+ int USER_IMPACT_LEVEL_40 = 40;
+ int USER_IMPACT_LEVEL_50 = 50;
+ int USER_IMPACT_LEVEL_70 = 70;
+ /* Action has high user impact, a last resort, user of a device will be very frustrated. */
+ int USER_IMPACT_LEVEL_71 = 71;
+ int USER_IMPACT_LEVEL_75 = 75;
+ int USER_IMPACT_LEVEL_80 = 80;
+ int USER_IMPACT_LEVEL_90 = 90;
+ int USER_IMPACT_LEVEL_100 = 100;
+ }
+
+ /** Register instances of this interface to receive notifications on package failure. */
+ public interface PackageHealthObserver {
+ /**
+ * Called when health check fails for the {@code versionedPackage}.
+ *
+ * @param versionedPackage the package that is failing. This may be null if a native
+ * service is crashing.
+ * @param failureReason the type of failure that is occurring.
+ * @param mitigationCount the number of times mitigation has been called for this package
+ * (including this time).
+ *
+ *
+ * @return any one of {@link PackageHealthObserverImpact} to express the impact
+ * to the user on {@link #execute}
+ */
+ @PackageHealthObserverImpact int onHealthCheckFailed(
+ @Nullable VersionedPackage versionedPackage,
+ @FailureReasons int failureReason,
+ int mitigationCount);
+
+ /**
+ * Executes mitigation for {@link #onHealthCheckFailed}.
+ *
+ * @param versionedPackage the package that is failing. This may be null if a native
+ * service is crashing.
+ * @param failureReason the type of failure that is occurring.
+ * @param mitigationCount the number of times mitigation has been called for this package
+ * (including this time).
+ * @return {@code true} if action was executed successfully, {@code false} otherwise
+ */
+ boolean execute(@Nullable VersionedPackage versionedPackage,
+ @FailureReasons int failureReason, int mitigationCount);
+
+
+ /**
+ * Called when the system server has booted several times within a window of time, defined
+ * by {@link #mBootThreshold}
+ *
+ * @param mitigationCount the number of times mitigation has been attempted for this
+ * boot loop (including this time).
+ */
+ default @PackageHealthObserverImpact int onBootLoop(int mitigationCount) {
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+
+ /**
+ * Executes mitigation for {@link #onBootLoop}
+ * @param mitigationCount the number of times mitigation has been attempted for this
+ * boot loop (including this time).
+ */
+ default boolean executeBootLoopMitigation(int mitigationCount) {
+ return false;
+ }
+
+ // TODO(b/120598832): Ensure uniqueness?
+ /**
+ * Identifier for the observer, should not change across device updates otherwise the
+ * watchdog may drop observing packages with the old name.
+ */
+ @NonNull String getUniqueIdentifier();
+
+ /**
+ * An observer will not be pruned if this is set, even if the observer is not explicitly
+ * monitoring any packages.
+ */
+ default boolean isPersistent() {
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if this observer wishes to observe the given package, {@code false}
+ * otherwise
+ *
+ * <p> A persistent observer may choose to start observing certain failing packages, even if
+ * it has not explicitly asked to watch the package with {@link #startObservingHealth}.
+ */
+ default boolean mayObservePackage(@NonNull String packageName) {
+ return false;
+ }
+ }
+
+ @VisibleForTesting
+ long getTriggerFailureCount() {
+ synchronized (mLock) {
+ return mTriggerFailureCount;
+ }
+ }
+
+ @VisibleForTesting
+ long getTriggerFailureDurationMs() {
+ synchronized (mLock) {
+ return mTriggerFailureDurationMs;
+ }
+ }
+
+ /**
+ * Serializes and syncs health check requests with the {@link ExplicitHealthCheckController}.
+ */
+ private void syncRequestsAsync() {
+ mShortTaskHandler.removeCallbacks(mSyncRequests);
+ mShortTaskHandler.post(mSyncRequests);
+ }
+
+ /**
+ * Syncs health check requests with the {@link ExplicitHealthCheckController}.
+ * Calls to this must be serialized.
+ *
+ * @see #syncRequestsAsync
+ */
+ private void syncRequests() {
+ boolean syncRequired = false;
+ synchronized (mLock) {
+ if (mIsPackagesReady) {
+ Set<String> packages = getPackagesPendingHealthChecksLocked();
+ if (mSyncRequired || !packages.equals(mRequestedHealthCheckPackages)
+ || packages.isEmpty()) {
+ syncRequired = true;
+ mRequestedHealthCheckPackages = packages;
+ }
+ } // else, we will sync requests when packages become ready
+ }
+
+ // Call outside lock to avoid holding lock when calling into the controller.
+ if (syncRequired) {
+ Slog.i(TAG, "Syncing health check requests for packages: "
+ + mRequestedHealthCheckPackages);
+ mHealthCheckController.syncRequests(mRequestedHealthCheckPackages);
+ mSyncRequired = false;
+ }
+ }
+
+ /**
+ * Updates the observers monitoring {@code packageName} that explicit health check has passed.
+ *
+ * <p> This update is strictly for registered observers at the time of the call
+ * Observers that register after this signal will have no knowledge of prior signals and will
+ * effectively behave as if the explicit health check hasn't passed for {@code packageName}.
+ *
+ * <p> {@code packageName} can still be considered failed if reported by
+ * {@link #onPackageFailureLocked} before the package expires.
+ *
+ * <p> Triggered by components outside the system server when they are fully functional after an
+ * update.
+ */
+ private void onHealthCheckPassed(String packageName) {
+ Slog.i(TAG, "Health check passed for package: " + packageName);
+ boolean isStateChanged = false;
+
+ synchronized (mLock) {
+ for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) {
+ ObserverInternal observer = mAllObservers.valueAt(observerIdx);
+ MonitoredPackage monitoredPackage = observer.getMonitoredPackage(packageName);
+
+ if (monitoredPackage != null) {
+ int oldState = monitoredPackage.getHealthCheckStateLocked();
+ int newState = monitoredPackage.tryPassHealthCheckLocked();
+ isStateChanged |= oldState != newState;
+ }
+ }
+ }
+
+ if (isStateChanged) {
+ syncState("health check passed for " + packageName);
+ }
+ }
+
+ private void onSupportedPackages(List<PackageConfig> supportedPackages) {
+ boolean isStateChanged = false;
+
+ Map<String, Long> supportedPackageTimeouts = new ArrayMap<>();
+ Iterator<PackageConfig> it = supportedPackages.iterator();
+ while (it.hasNext()) {
+ PackageConfig info = it.next();
+ supportedPackageTimeouts.put(info.getPackageName(), info.getHealthCheckTimeoutMillis());
+ }
+
+ synchronized (mLock) {
+ Slog.d(TAG, "Received supported packages " + supportedPackages);
+ Iterator<ObserverInternal> oit = mAllObservers.values().iterator();
+ while (oit.hasNext()) {
+ Iterator<MonitoredPackage> pit = oit.next().getMonitoredPackages()
+ .values().iterator();
+ while (pit.hasNext()) {
+ MonitoredPackage monitoredPackage = pit.next();
+ String packageName = monitoredPackage.getName();
+ int oldState = monitoredPackage.getHealthCheckStateLocked();
+ int newState;
+
+ if (supportedPackageTimeouts.containsKey(packageName)) {
+ // Supported packages become ACTIVE if currently INACTIVE
+ newState = monitoredPackage.setHealthCheckActiveLocked(
+ supportedPackageTimeouts.get(packageName));
+ } else {
+ // Unsupported packages are marked as PASSED unless already FAILED
+ newState = monitoredPackage.tryPassHealthCheckLocked();
+ }
+ isStateChanged |= oldState != newState;
+ }
+ }
+ }
+
+ if (isStateChanged) {
+ syncState("updated health check supported packages " + supportedPackages);
+ }
+ }
+
+ private void onSyncRequestNotified() {
+ synchronized (mLock) {
+ mSyncRequired = true;
+ syncRequestsAsync();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private Set<String> getPackagesPendingHealthChecksLocked() {
+ Set<String> packages = new ArraySet<>();
+ Iterator<ObserverInternal> oit = mAllObservers.values().iterator();
+ while (oit.hasNext()) {
+ ObserverInternal observer = oit.next();
+ Iterator<MonitoredPackage> pit =
+ observer.getMonitoredPackages().values().iterator();
+ while (pit.hasNext()) {
+ MonitoredPackage monitoredPackage = pit.next();
+ String packageName = monitoredPackage.getName();
+ if (monitoredPackage.isPendingHealthChecksLocked()) {
+ packages.add(packageName);
+ }
+ }
+ }
+ return packages;
+ }
+
+ /**
+ * Syncs the state of the observers.
+ *
+ * <p> Prunes all observers, saves new state to disk, syncs health check requests with the
+ * health check service and schedules the next state sync.
+ */
+ private void syncState(String reason) {
+ synchronized (mLock) {
+ Slog.i(TAG, "Syncing state, reason: " + reason);
+ pruneObserversLocked();
+
+ saveToFileAsync();
+ syncRequestsAsync();
+
+ // Done syncing state, schedule the next state sync
+ scheduleNextSyncStateLocked();
+ }
+ }
+
+ private void syncStateWithScheduledReason() {
+ syncState("scheduled");
+ }
+
+ @GuardedBy("mLock")
+ private void scheduleNextSyncStateLocked() {
+ long durationMs = getNextStateSyncMillisLocked();
+ mShortTaskHandler.removeCallbacks(mSyncStateWithScheduledReason);
+ if (durationMs == Long.MAX_VALUE) {
+ Slog.i(TAG, "Cancelling state sync, nothing to sync");
+ mUptimeAtLastStateSync = 0;
+ } else {
+ mUptimeAtLastStateSync = mSystemClock.uptimeMillis();
+ mShortTaskHandler.postDelayed(mSyncStateWithScheduledReason, durationMs);
+ }
+ }
+
+ /**
+ * Returns the next duration in millis to sync the watchdog state.
+ *
+ * @returns Long#MAX_VALUE if there are no observed packages.
+ */
+ @GuardedBy("mLock")
+ private long getNextStateSyncMillisLocked() {
+ long shortestDurationMs = Long.MAX_VALUE;
+ for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
+ ArrayMap<String, MonitoredPackage> packages = mAllObservers.valueAt(oIndex)
+ .getMonitoredPackages();
+ for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
+ MonitoredPackage mp = packages.valueAt(pIndex);
+ long duration = mp.getShortestScheduleDurationMsLocked();
+ if (duration < shortestDurationMs) {
+ shortestDurationMs = duration;
+ }
+ }
+ }
+ return shortestDurationMs;
+ }
+
+ /**
+ * Removes {@code elapsedMs} milliseconds from all durations on monitored packages
+ * and updates other internal state.
+ */
+ @GuardedBy("mLock")
+ private void pruneObserversLocked() {
+ long elapsedMs = mUptimeAtLastStateSync == 0
+ ? 0 : mSystemClock.uptimeMillis() - mUptimeAtLastStateSync;
+ if (elapsedMs <= 0) {
+ Slog.i(TAG, "Not pruning observers, elapsed time: " + elapsedMs + "ms");
+ return;
+ }
+
+ Iterator<ObserverInternal> it = mAllObservers.values().iterator();
+ while (it.hasNext()) {
+ ObserverInternal observer = it.next();
+ Set<MonitoredPackage> failedPackages =
+ observer.prunePackagesLocked(elapsedMs);
+ if (!failedPackages.isEmpty()) {
+ onHealthCheckFailed(observer, failedPackages);
+ }
+ if (observer.getMonitoredPackages().isEmpty() && (observer.registeredObserver == null
+ || !observer.registeredObserver.isPersistent())) {
+ Slog.i(TAG, "Discarding observer " + observer.name + ". All packages expired");
+ it.remove();
+ }
+ }
+ }
+
+ private void onHealthCheckFailed(ObserverInternal observer,
+ Set<MonitoredPackage> failedPackages) {
+ mLongTaskHandler.post(() -> {
+ synchronized (mLock) {
+ PackageHealthObserver registeredObserver = observer.registeredObserver;
+ if (registeredObserver != null) {
+ Iterator<MonitoredPackage> it = failedPackages.iterator();
+ while (it.hasNext()) {
+ VersionedPackage versionedPkg = getVersionedPackage(it.next().getName());
+ if (versionedPkg != null) {
+ Slog.i(TAG,
+ "Explicit health check failed for package " + versionedPkg);
+ registeredObserver.execute(versionedPkg,
+ PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK, 1);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Gets PackageInfo for the given package. Matches any user and apex.
+ *
+ * @throws PackageManager.NameNotFoundException if no such package is installed.
+ */
+ private PackageInfo getPackageInfo(String packageName)
+ throws PackageManager.NameNotFoundException {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ // The MATCH_ANY_USER flag doesn't mix well with the MATCH_APEX
+ // flag, so make two separate attempts to get the package info.
+ // We don't need both flags at the same time because we assume
+ // apex files are always installed for all users.
+ return pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER);
+ } catch (PackageManager.NameNotFoundException e) {
+ return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
+ }
+ }
+
+ @Nullable
+ private VersionedPackage getVersionedPackage(String packageName) {
+ final PackageManager pm = mContext.getPackageManager();
+ if (pm == null || TextUtils.isEmpty(packageName)) {
+ return null;
+ }
+ try {
+ final long versionCode = getPackageInfo(packageName).getLongVersionCode();
+ return new VersionedPackage(packageName, versionCode);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Loads mAllObservers from file.
+ *
+ * <p>Note that this is <b>not</b> thread safe and should only called be called
+ * from the constructor.
+ */
+ private void loadFromFile() {
+ InputStream infile = null;
+ mAllObservers.clear();
+ try {
+ infile = mPolicyFile.openRead();
+ final TypedXmlPullParser parser = Xml.resolvePullParser(infile);
+ XmlUtils.beginDocument(parser, TAG_PACKAGE_WATCHDOG);
+ int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ ObserverInternal observer = ObserverInternal.read(parser, this);
+ if (observer != null) {
+ mAllObservers.put(observer.name, observer);
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // Nothing to monitor
+ } catch (IOException | NumberFormatException | XmlPullParserException e) {
+ Slog.wtf(TAG, "Unable to read monitored packages, deleting file", e);
+ mPolicyFile.delete();
+ } finally {
+ IoUtils.closeQuietly(infile);
+ }
+ }
+
+ private void onPropertyChanged(DeviceConfig.Properties properties) {
+ try {
+ updateConfigs();
+ } catch (Exception ignore) {
+ Slog.w(TAG, "Failed to reload device config changes");
+ }
+ }
+
+ /** Adds a {@link DeviceConfig#OnPropertiesChangedListener}. */
+ private void setPropertyChangedListenerLocked() {
+ DeviceConfig.addOnPropertiesChangedListener(
+ DeviceConfig.NAMESPACE_ROLLBACK,
+ mContext.getMainExecutor(),
+ mOnPropertyChangedListener);
+ }
+
+ @VisibleForTesting
+ void removePropertyChangedListener() {
+ DeviceConfig.removeOnPropertiesChangedListener(mOnPropertyChangedListener);
+ }
+
+ /**
+ * Health check is enabled or disabled after reading the flags
+ * from DeviceConfig.
+ */
+ @VisibleForTesting
+ void updateConfigs() {
+ synchronized (mLock) {
+ mTriggerFailureCount = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_ROLLBACK,
+ PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT,
+ DEFAULT_TRIGGER_FAILURE_COUNT);
+ if (mTriggerFailureCount <= 0) {
+ mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT;
+ }
+
+ mTriggerFailureDurationMs = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_ROLLBACK,
+ PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS,
+ DEFAULT_TRIGGER_FAILURE_DURATION_MS);
+ if (mTriggerFailureDurationMs <= 0) {
+ mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS;
+ }
+
+ setExplicitHealthCheckEnabled(DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ROLLBACK,
+ PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED,
+ DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED));
+ }
+ }
+
+ /**
+ * Persists mAllObservers to file. Threshold information is ignored.
+ */
+ private boolean saveToFile() {
+ Slog.i(TAG, "Saving observer state to file");
+ synchronized (mLock) {
+ FileOutputStream stream;
+ try {
+ stream = mPolicyFile.startWrite();
+ } catch (IOException e) {
+ Slog.w(TAG, "Cannot update monitored packages", e);
+ return false;
+ }
+
+ try {
+ TypedXmlSerializer out = Xml.resolveSerializer(stream);
+ out.startDocument(null, true);
+ out.startTag(null, TAG_PACKAGE_WATCHDOG);
+ out.attributeInt(null, ATTR_VERSION, DB_VERSION);
+ for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
+ mAllObservers.valueAt(oIndex).writeLocked(out);
+ }
+ out.endTag(null, TAG_PACKAGE_WATCHDOG);
+ out.endDocument();
+ mPolicyFile.finishWrite(stream);
+ return true;
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to save monitored packages, restoring backup", e);
+ mPolicyFile.failWrite(stream);
+ return false;
+ } finally {
+ IoUtils.closeQuietly(stream);
+ }
+ }
+ }
+
+ private void saveToFileAsync() {
+ if (!mLongTaskHandler.hasCallbacks(mSaveToFile)) {
+ mLongTaskHandler.post(mSaveToFile);
+ }
+ }
+
+ /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */
+ public static String longArrayQueueToString(LongArrayQueue queue) {
+ if (queue.size() > 0) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(queue.get(0));
+ for (int i = 1; i < queue.size(); i++) {
+ sb.append(",");
+ sb.append(queue.get(i));
+ }
+ return sb.toString();
+ }
+ return "";
+ }
+
+ /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */
+ public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) {
+ LongArrayQueue result = new LongArrayQueue();
+ if (!TextUtils.isEmpty(commaSeparatedValues)) {
+ String[] values = commaSeparatedValues.split(",");
+ for (String value : values) {
+ result.addLast(Long.parseLong(value));
+ }
+ }
+ return result;
+ }
+
+
+ /** Dump status of every observer in mAllObservers. */
+ public void dump(@NonNull PrintWriter pw) {
+ if (Flags.synchronousRebootInRescueParty() && RescueParty.isRecoveryTriggeredReboot()) {
+ dumpInternal(pw);
+ } else {
+ synchronized (mLock) {
+ dumpInternal(pw);
+ }
+ }
+ }
+
+ private void dumpInternal(@NonNull PrintWriter pw) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println("Package Watchdog status");
+ ipw.increaseIndent();
+ synchronized (mLock) {
+ for (String observerName : mAllObservers.keySet()) {
+ ipw.println("Observer name: " + observerName);
+ ipw.increaseIndent();
+ ObserverInternal observerInternal = mAllObservers.get(observerName);
+ observerInternal.dump(ipw);
+ ipw.decreaseIndent();
+ }
+ }
+ ipw.decreaseIndent();
+ dumpCrashRecoveryEvents(ipw);
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void registerObserverInternal(ObserverInternal observerInternal) {
+ mAllObservers.put(observerInternal.name, observerInternal);
+ }
+
+ /**
+ * Represents an observer monitoring a set of packages along with the failure thresholds for
+ * each package.
+ *
+ * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing
+ * instances of this class.
+ */
+ static class ObserverInternal {
+ public final String name;
+ @GuardedBy("mLock")
+ private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>();
+ @Nullable
+ @GuardedBy("mLock")
+ public PackageHealthObserver registeredObserver;
+ private int mMitigationCount;
+
+ ObserverInternal(String name, List<MonitoredPackage> packages) {
+ this(name, packages, /*mitigationCount=*/ 0);
+ }
+
+ ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) {
+ this.name = name;
+ updatePackagesLocked(packages);
+ this.mMitigationCount = mitigationCount;
+ }
+
+ /**
+ * Writes important {@link MonitoredPackage} details for this observer to file.
+ * Does not persist any package failure thresholds.
+ */
+ @GuardedBy("mLock")
+ public boolean writeLocked(TypedXmlSerializer out) {
+ try {
+ out.startTag(null, TAG_OBSERVER);
+ out.attribute(null, ATTR_NAME, name);
+ if (Flags.recoverabilityDetection()) {
+ out.attributeInt(null, ATTR_MITIGATION_COUNT, mMitigationCount);
+ }
+ for (int i = 0; i < mPackages.size(); i++) {
+ MonitoredPackage p = mPackages.valueAt(i);
+ p.writeLocked(out);
+ }
+ out.endTag(null, TAG_OBSERVER);
+ return true;
+ } catch (IOException e) {
+ Slog.w(TAG, "Cannot save observer", e);
+ return false;
+ }
+ }
+
+ public int getBootMitigationCount() {
+ return mMitigationCount;
+ }
+
+ public void setBootMitigationCount(int mitigationCount) {
+ mMitigationCount = mitigationCount;
+ }
+
+ @GuardedBy("mLock")
+ public void updatePackagesLocked(List<MonitoredPackage> packages) {
+ for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
+ MonitoredPackage p = packages.get(pIndex);
+ MonitoredPackage existingPackage = getMonitoredPackage(p.getName());
+ if (existingPackage != null) {
+ existingPackage.updateHealthCheckDuration(p.mDurationMs);
+ } else {
+ putMonitoredPackage(p);
+ }
+ }
+ }
+
+ /**
+ * Reduces the monitoring durations of all packages observed by this observer by
+ * {@code elapsedMs}. If any duration is less than 0, the package is removed from
+ * observation. If any health check duration is less than 0, the health check result
+ * is evaluated.
+ *
+ * @return a {@link Set} of packages that were removed from the observer without explicit
+ * health check passing, or an empty list if no package expired for which an explicit health
+ * check was still pending
+ */
+ @GuardedBy("mLock")
+ private Set<MonitoredPackage> prunePackagesLocked(long elapsedMs) {
+ Set<MonitoredPackage> failedPackages = new ArraySet<>();
+ Iterator<MonitoredPackage> it = mPackages.values().iterator();
+ while (it.hasNext()) {
+ MonitoredPackage p = it.next();
+ int oldState = p.getHealthCheckStateLocked();
+ int newState = p.handleElapsedTimeLocked(elapsedMs);
+ if (oldState != HealthCheckState.FAILED
+ && newState == HealthCheckState.FAILED) {
+ Slog.i(TAG, "Package " + p.getName() + " failed health check");
+ failedPackages.add(p);
+ }
+ if (p.isExpiredLocked()) {
+ it.remove();
+ }
+ }
+ return failedPackages;
+ }
+
+ /**
+ * Increments failure counts of {@code packageName}.
+ * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise
+ * @hide
+ */
+ @GuardedBy("mLock")
+ public boolean onPackageFailureLocked(String packageName) {
+ if (getMonitoredPackage(packageName) == null && registeredObserver.isPersistent()
+ && registeredObserver.mayObservePackage(packageName)) {
+ putMonitoredPackage(sPackageWatchdog.newMonitoredPackage(
+ packageName, DEFAULT_OBSERVING_DURATION_MS, false));
+ }
+ MonitoredPackage p = getMonitoredPackage(packageName);
+ if (p != null) {
+ return p.onFailureLocked();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the map of packages monitored by this observer.
+ *
+ * @return a mapping of package names to {@link MonitoredPackage} objects.
+ */
+ @GuardedBy("mLock")
+ public ArrayMap<String, MonitoredPackage> getMonitoredPackages() {
+ return mPackages;
+ }
+
+ /**
+ * Returns the {@link MonitoredPackage} associated with a given package name if the
+ * package is being monitored by this observer.
+ *
+ * @param packageName: the name of the package.
+ * @return the {@link MonitoredPackage} object associated with the package name if one
+ * exists, {@code null} otherwise.
+ */
+ @GuardedBy("mLock")
+ @Nullable
+ public MonitoredPackage getMonitoredPackage(String packageName) {
+ return mPackages.get(packageName);
+ }
+
+ /**
+ * Associates a {@link MonitoredPackage} with the observer.
+ *
+ * @param p: the {@link MonitoredPackage} to store.
+ */
+ @GuardedBy("mLock")
+ public void putMonitoredPackage(MonitoredPackage p) {
+ mPackages.put(p.getName(), p);
+ }
+
+ /**
+ * Returns one ObserverInternal from the {@code parser} and advances its state.
+ *
+ * <p>Note that this method is <b>not</b> thread safe. It should only be called from
+ * #loadFromFile which in turn is only called on construction of the
+ * singleton PackageWatchdog.
+ **/
+ public static ObserverInternal read(TypedXmlPullParser parser, PackageWatchdog watchdog) {
+ String observerName = null;
+ int observerMitigationCount = 0;
+ if (TAG_OBSERVER.equals(parser.getName())) {
+ observerName = parser.getAttributeValue(null, ATTR_NAME);
+ if (TextUtils.isEmpty(observerName)) {
+ Slog.wtf(TAG, "Unable to read observer name");
+ return null;
+ }
+ }
+ List<MonitoredPackage> packages = new ArrayList<>();
+ int innerDepth = parser.getDepth();
+ try {
+ if (Flags.recoverabilityDetection()) {
+ try {
+ observerMitigationCount =
+ parser.getAttributeInt(null, ATTR_MITIGATION_COUNT);
+ } catch (XmlPullParserException e) {
+ Slog.i(
+ TAG,
+ "ObserverInternal mitigation count was not present.");
+ }
+ }
+ while (XmlUtils.nextElementWithin(parser, innerDepth)) {
+ if (TAG_PACKAGE.equals(parser.getName())) {
+ try {
+ MonitoredPackage pkg = watchdog.parseMonitoredPackage(parser);
+ if (pkg != null) {
+ packages.add(pkg);
+ }
+ } catch (NumberFormatException e) {
+ Slog.wtf(TAG, "Skipping package for observer " + observerName, e);
+ continue;
+ }
+ }
+ }
+ } catch (XmlPullParserException | IOException e) {
+ Slog.wtf(TAG, "Unable to read observer " + observerName, e);
+ return null;
+ }
+ if (packages.isEmpty()) {
+ return null;
+ }
+ return new ObserverInternal(observerName, packages, observerMitigationCount);
+ }
+
+ /** Dumps information about this observer and the packages it watches. */
+ public void dump(IndentingPrintWriter pw) {
+ boolean isPersistent = registeredObserver != null && registeredObserver.isPersistent();
+ pw.println("Persistent: " + isPersistent);
+ for (String packageName : mPackages.keySet()) {
+ MonitoredPackage p = getMonitoredPackage(packageName);
+ pw.println(packageName + ": ");
+ pw.increaseIndent();
+ pw.println("# Failures: " + p.mFailureHistory.size());
+ pw.println("Monitoring duration remaining: " + p.mDurationMs + "ms");
+ pw.println("Explicit health check duration: " + p.mHealthCheckDurationMs + "ms");
+ pw.println("Health check state: " + p.toString(p.mHealthCheckState));
+ pw.decreaseIndent();
+ }
+ }
+ }
+
+ /** @hide */
+ @Retention(SOURCE)
+ @IntDef(value = {
+ HealthCheckState.ACTIVE,
+ HealthCheckState.INACTIVE,
+ HealthCheckState.PASSED,
+ HealthCheckState.FAILED})
+ public @interface HealthCheckState {
+ // The package has not passed health check but has requested a health check
+ int ACTIVE = 0;
+ // The package has not passed health check and has not requested a health check
+ int INACTIVE = 1;
+ // The package has passed health check
+ int PASSED = 2;
+ // The package has failed health check
+ int FAILED = 3;
+ }
+
+ MonitoredPackage newMonitoredPackage(
+ String name, long durationMs, boolean hasPassedHealthCheck) {
+ return newMonitoredPackage(name, durationMs, Long.MAX_VALUE, hasPassedHealthCheck,
+ new LongArrayQueue());
+ }
+
+ MonitoredPackage newMonitoredPackage(String name, long durationMs, long healthCheckDurationMs,
+ boolean hasPassedHealthCheck, LongArrayQueue mitigationCalls) {
+ return new MonitoredPackage(name, durationMs, healthCheckDurationMs,
+ hasPassedHealthCheck, mitigationCalls);
+ }
+
+ MonitoredPackage parseMonitoredPackage(TypedXmlPullParser parser)
+ throws XmlPullParserException {
+ String packageName = parser.getAttributeValue(null, ATTR_NAME);
+ long duration = parser.getAttributeLong(null, ATTR_DURATION);
+ long healthCheckDuration = parser.getAttributeLong(null,
+ ATTR_EXPLICIT_HEALTH_CHECK_DURATION);
+ boolean hasPassedHealthCheck = parser.getAttributeBoolean(null, ATTR_PASSED_HEALTH_CHECK);
+ LongArrayQueue mitigationCalls = parseLongArrayQueue(
+ parser.getAttributeValue(null, ATTR_MITIGATION_CALLS));
+ return newMonitoredPackage(packageName,
+ duration, healthCheckDuration, hasPassedHealthCheck, mitigationCalls);
+ }
+
+ /**
+ * Represents a package and its health check state along with the time
+ * it should be monitored for.
+ *
+ * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing
+ * instances of this class.
+ */
+ class MonitoredPackage {
+ private final String mPackageName;
+ // Times when package failures happen sorted in ascending order
+ @GuardedBy("mLock")
+ private final LongArrayQueue mFailureHistory = new LongArrayQueue();
+ // Times when an observer was called to mitigate this package's failure. Sorted in
+ // ascending order.
+ @GuardedBy("mLock")
+ private final LongArrayQueue mMitigationCalls;
+ // One of STATE_[ACTIVE|INACTIVE|PASSED|FAILED]. Updated on construction and after
+ // methods that could change the health check state: handleElapsedTimeLocked and
+ // tryPassHealthCheckLocked
+ private int mHealthCheckState = HealthCheckState.INACTIVE;
+ // Whether an explicit health check has passed.
+ // This value in addition with mHealthCheckDurationMs determines the health check state
+ // of the package, see #getHealthCheckStateLocked
+ @GuardedBy("mLock")
+ private boolean mHasPassedHealthCheck;
+ // System uptime duration to monitor package.
+ @GuardedBy("mLock")
+ private long mDurationMs;
+ // System uptime duration to check the result of an explicit health check
+ // Initially, MAX_VALUE until we get a value from the health check service
+ // and request health checks.
+ // This value in addition with mHasPassedHealthCheck determines the health check state
+ // of the package, see #getHealthCheckStateLocked
+ @GuardedBy("mLock")
+ private long mHealthCheckDurationMs = Long.MAX_VALUE;
+
+ MonitoredPackage(String packageName, long durationMs,
+ long healthCheckDurationMs, boolean hasPassedHealthCheck,
+ LongArrayQueue mitigationCalls) {
+ mPackageName = packageName;
+ mDurationMs = durationMs;
+ mHealthCheckDurationMs = healthCheckDurationMs;
+ mHasPassedHealthCheck = hasPassedHealthCheck;
+ mMitigationCalls = mitigationCalls;
+ updateHealthCheckStateLocked();
+ }
+
+ /** Writes the salient fields to disk using {@code out}.
+ * @hide
+ */
+ @GuardedBy("mLock")
+ public void writeLocked(TypedXmlSerializer out) throws IOException {
+ out.startTag(null, TAG_PACKAGE);
+ out.attribute(null, ATTR_NAME, getName());
+ out.attributeLong(null, ATTR_DURATION, mDurationMs);
+ out.attributeLong(null, ATTR_EXPLICIT_HEALTH_CHECK_DURATION, mHealthCheckDurationMs);
+ out.attributeBoolean(null, ATTR_PASSED_HEALTH_CHECK, mHasPassedHealthCheck);
+ LongArrayQueue normalizedCalls = normalizeMitigationCalls();
+ out.attribute(null, ATTR_MITIGATION_CALLS, longArrayQueueToString(normalizedCalls));
+ out.endTag(null, TAG_PACKAGE);
+ }
+
+ /**
+ * Increment package failures or resets failure count depending on the last package failure.
+ *
+ * @return {@code true} if failure count exceeds a threshold, {@code false} otherwise
+ */
+ @GuardedBy("mLock")
+ public boolean onFailureLocked() {
+ // Sliding window algorithm: find out if there exists a window containing failures >=
+ // mTriggerFailureCount.
+ final long now = mSystemClock.uptimeMillis();
+ mFailureHistory.addLast(now);
+ while (now - mFailureHistory.peekFirst() > mTriggerFailureDurationMs) {
+ // Prune values falling out of the window
+ mFailureHistory.removeFirst();
+ }
+ boolean failed = mFailureHistory.size() >= mTriggerFailureCount;
+ if (failed) {
+ mFailureHistory.clear();
+ }
+ return failed;
+ }
+
+ /**
+ * Notes the timestamp of a mitigation call into the observer.
+ */
+ @GuardedBy("mLock")
+ public void noteMitigationCallLocked() {
+ mMitigationCalls.addLast(mSystemClock.uptimeMillis());
+ }
+
+ /**
+ * Prunes any mitigation calls outside of the de-escalation window, and returns the
+ * number of calls that are in the window afterwards.
+ *
+ * @return the number of mitigation calls made in the de-escalation window.
+ */
+ @GuardedBy("mLock")
+ public int getMitigationCountLocked() {
+ try {
+ final long now = mSystemClock.uptimeMillis();
+ while (now - mMitigationCalls.peekFirst() > DEFAULT_DEESCALATION_WINDOW_MS) {
+ mMitigationCalls.removeFirst();
+ }
+ } catch (NoSuchElementException ignore) {
+ }
+
+ return mMitigationCalls.size();
+ }
+
+ /**
+ * Before writing to disk, make the mitigation call timestamps relative to the current
+ * system uptime. This is because they need to be relative to the uptime which will reset
+ * at the next boot.
+ *
+ * @return a LongArrayQueue of the mitigation calls relative to the current system uptime.
+ */
+ @GuardedBy("mLock")
+ public LongArrayQueue normalizeMitigationCalls() {
+ LongArrayQueue normalized = new LongArrayQueue();
+ final long now = mSystemClock.uptimeMillis();
+ for (int i = 0; i < mMitigationCalls.size(); i++) {
+ normalized.addLast(mMitigationCalls.get(i) - now);
+ }
+ return normalized;
+ }
+
+ /**
+ * Sets the initial health check duration.
+ *
+ * @return the new health check state
+ */
+ @GuardedBy("mLock")
+ public int setHealthCheckActiveLocked(long initialHealthCheckDurationMs) {
+ if (initialHealthCheckDurationMs <= 0) {
+ Slog.wtf(TAG, "Cannot set non-positive health check duration "
+ + initialHealthCheckDurationMs + "ms for package " + getName()
+ + ". Using total duration " + mDurationMs + "ms instead");
+ initialHealthCheckDurationMs = mDurationMs;
+ }
+ if (mHealthCheckState == HealthCheckState.INACTIVE) {
+ // Transitions to ACTIVE
+ mHealthCheckDurationMs = initialHealthCheckDurationMs;
+ }
+ return updateHealthCheckStateLocked();
+ }
+
+ /**
+ * Updates the monitoring durations of the package.
+ *
+ * @return the new health check state
+ */
+ @GuardedBy("mLock")
+ public int handleElapsedTimeLocked(long elapsedMs) {
+ if (elapsedMs <= 0) {
+ Slog.w(TAG, "Cannot handle non-positive elapsed time for package " + getName());
+ return mHealthCheckState;
+ }
+ // Transitions to FAILED if now <= 0 and health check not passed
+ mDurationMs -= elapsedMs;
+ if (mHealthCheckState == HealthCheckState.ACTIVE) {
+ // We only update health check durations if we have #setHealthCheckActiveLocked
+ // This ensures we don't leave the INACTIVE state for an unexpected elapsed time
+ // Transitions to FAILED if now <= 0 and health check not passed
+ mHealthCheckDurationMs -= elapsedMs;
+ }
+ return updateHealthCheckStateLocked();
+ }
+
+ /** Explicitly update the monitoring duration of the package. */
+ @GuardedBy("mLock")
+ public void updateHealthCheckDuration(long newDurationMs) {
+ mDurationMs = newDurationMs;
+ }
+
+ /**
+ * Marks the health check as passed and transitions to {@link HealthCheckState.PASSED}
+ * if not yet {@link HealthCheckState.FAILED}.
+ *
+ * @return the new {@link HealthCheckState health check state}
+ */
+ @GuardedBy("mLock")
+ @HealthCheckState
+ public int tryPassHealthCheckLocked() {
+ if (mHealthCheckState != HealthCheckState.FAILED) {
+ // FAILED is a final state so only pass if we haven't failed
+ // Transition to PASSED
+ mHasPassedHealthCheck = true;
+ }
+ return updateHealthCheckStateLocked();
+ }
+
+ /** Returns the monitored package name. */
+ private String getName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the current {@link HealthCheckState health check state}.
+ */
+ @GuardedBy("mLock")
+ @HealthCheckState
+ public int getHealthCheckStateLocked() {
+ return mHealthCheckState;
+ }
+
+ /**
+ * Returns the shortest duration before the package should be scheduled for a prune.
+ *
+ * @return the duration or {@link Long#MAX_VALUE} if the package should not be scheduled
+ */
+ @GuardedBy("mLock")
+ public long getShortestScheduleDurationMsLocked() {
+ // Consider health check duration only if #isPendingHealthChecksLocked is true
+ return Math.min(toPositive(mDurationMs),
+ isPendingHealthChecksLocked()
+ ? toPositive(mHealthCheckDurationMs) : Long.MAX_VALUE);
+ }
+
+ /**
+ * Returns {@code true} if the total duration left to monitor the package is less than or
+ * equal to 0 {@code false} otherwise.
+ */
+ @GuardedBy("mLock")
+ public boolean isExpiredLocked() {
+ return mDurationMs <= 0;
+ }
+
+ /**
+ * Returns {@code true} if the package, {@link #getName} is expecting health check results
+ * {@code false} otherwise.
+ */
+ @GuardedBy("mLock")
+ public boolean isPendingHealthChecksLocked() {
+ return mHealthCheckState == HealthCheckState.ACTIVE
+ || mHealthCheckState == HealthCheckState.INACTIVE;
+ }
+
+ /**
+ * Updates the health check state based on {@link #mHasPassedHealthCheck}
+ * and {@link #mHealthCheckDurationMs}.
+ *
+ * @return the new {@link HealthCheckState health check state}
+ */
+ @GuardedBy("mLock")
+ @HealthCheckState
+ private int updateHealthCheckStateLocked() {
+ int oldState = mHealthCheckState;
+ if (mHasPassedHealthCheck) {
+ // Set final state first to avoid ambiguity
+ mHealthCheckState = HealthCheckState.PASSED;
+ } else if (mHealthCheckDurationMs <= 0 || mDurationMs <= 0) {
+ // Set final state first to avoid ambiguity
+ mHealthCheckState = HealthCheckState.FAILED;
+ } else if (mHealthCheckDurationMs == Long.MAX_VALUE) {
+ mHealthCheckState = HealthCheckState.INACTIVE;
+ } else {
+ mHealthCheckState = HealthCheckState.ACTIVE;
+ }
+
+ if (oldState != mHealthCheckState) {
+ Slog.i(TAG, "Updated health check state for package " + getName() + ": "
+ + toString(oldState) + " -> " + toString(mHealthCheckState));
+ }
+ return mHealthCheckState;
+ }
+
+ /** Returns a {@link String} representation of the current health check state. */
+ private String toString(@HealthCheckState int state) {
+ switch (state) {
+ case HealthCheckState.ACTIVE:
+ return "ACTIVE";
+ case HealthCheckState.INACTIVE:
+ return "INACTIVE";
+ case HealthCheckState.PASSED:
+ return "PASSED";
+ case HealthCheckState.FAILED:
+ return "FAILED";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ /** Returns {@code value} if it is greater than 0 or {@link Long#MAX_VALUE} otherwise. */
+ private long toPositive(long value) {
+ return value > 0 ? value : Long.MAX_VALUE;
+ }
+
+ /** Compares the equality of this object with another {@link MonitoredPackage}. */
+ @VisibleForTesting
+ boolean isEqualTo(MonitoredPackage pkg) {
+ return (getName().equals(pkg.getName()))
+ && mDurationMs == pkg.mDurationMs
+ && mHasPassedHealthCheck == pkg.mHasPassedHealthCheck
+ && mHealthCheckDurationMs == pkg.mHealthCheckDurationMs
+ && (mMitigationCalls.toString()).equals(pkg.mMitigationCalls.toString());
+ }
+ }
+
+ @GuardedBy("mLock")
+ @SuppressWarnings("GuardedBy")
+ void saveAllObserversBootMitigationCountToMetadata(String filePath) {
+ HashMap<String, Integer> bootMitigationCounts = new HashMap<>();
+ for (int i = 0; i < mAllObservers.size(); i++) {
+ final ObserverInternal observer = mAllObservers.valueAt(i);
+ bootMitigationCounts.put(observer.name, observer.getBootMitigationCount());
+ }
+
+ try {
+ FileOutputStream fileStream = new FileOutputStream(new File(filePath));
+ ObjectOutputStream objectStream = new ObjectOutputStream(fileStream);
+ objectStream.writeObject(bootMitigationCounts);
+ objectStream.flush();
+ objectStream.close();
+ fileStream.close();
+ } catch (Exception e) {
+ Slog.i(TAG, "Could not save observers metadata to file: " + e);
+ }
+ }
+
+ /**
+ * Handles the thresholding logic for system server boots.
+ */
+ class BootThreshold {
+
+ private final int mBootTriggerCount;
+ private final long mTriggerWindow;
+
+ BootThreshold(int bootTriggerCount, long triggerWindow) {
+ this.mBootTriggerCount = bootTriggerCount;
+ this.mTriggerWindow = triggerWindow;
+ }
+
+ public void reset() {
+ setStart(0);
+ setCount(0);
+ }
+
+ protected int getCount() {
+ return CrashRecoveryProperties.rescueBootCount().orElse(0);
+ }
+
+ protected void setCount(int count) {
+ CrashRecoveryProperties.rescueBootCount(count);
+ }
+
+ public long getStart() {
+ return CrashRecoveryProperties.rescueBootStart().orElse(0L);
+ }
+
+ public int getMitigationCount() {
+ return CrashRecoveryProperties.bootMitigationCount().orElse(0);
+ }
+
+ public void setStart(long start) {
+ CrashRecoveryProperties.rescueBootStart(getStartTime(start));
+ }
+
+ public void setMitigationStart(long start) {
+ CrashRecoveryProperties.bootMitigationStart(getStartTime(start));
+ }
+
+ public long getMitigationStart() {
+ return CrashRecoveryProperties.bootMitigationStart().orElse(0L);
+ }
+
+ public void setMitigationCount(int count) {
+ CrashRecoveryProperties.bootMitigationCount(count);
+ }
+
+ private static long constrain(long amount, long low, long high) {
+ return amount < low ? low : (amount > high ? high : amount);
+ }
+
+ public long getStartTime(long start) {
+ final long now = mSystemClock.uptimeMillis();
+ return constrain(start, 0, now);
+ }
+
+ public void saveMitigationCountToMetadata() {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(METADATA_FILE))) {
+ writer.write(String.valueOf(getMitigationCount()));
+ } catch (Exception e) {
+ Slog.e(TAG, "Could not save metadata to file: " + e);
+ }
+ }
+
+ public void readMitigationCountFromMetadataIfNecessary() {
+ File bootPropsFile = new File(METADATA_FILE);
+ if (bootPropsFile.exists()) {
+ try (BufferedReader reader = new BufferedReader(new FileReader(METADATA_FILE))) {
+ String mitigationCount = reader.readLine();
+ setMitigationCount(Integer.parseInt(mitigationCount));
+ bootPropsFile.delete();
+ } catch (Exception e) {
+ Slog.i(TAG, "Could not read metadata file: " + e);
+ }
+ }
+ }
+
+
+ /** Increments the boot counter, and returns whether the device is bootlooping. */
+ @GuardedBy("mLock")
+ public boolean incrementAndTest() {
+ if (Flags.recoverabilityDetection()) {
+ readAllObserversBootMitigationCountIfNecessary(METADATA_FILE);
+ } else {
+ readMitigationCountFromMetadataIfNecessary();
+ }
+
+ final long now = mSystemClock.uptimeMillis();
+ if (now - getStart() < 0) {
+ Slog.e(TAG, "Window was less than zero. Resetting start to current time.");
+ setStart(now);
+ setMitigationStart(now);
+ }
+ if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) {
+ setMitigationStart(now);
+ if (Flags.recoverabilityDetection()) {
+ resetAllObserversBootMitigationCount();
+ } else {
+ setMitigationCount(0);
+ }
+ }
+ final long window = now - getStart();
+ if (window >= mTriggerWindow) {
+ setCount(1);
+ setStart(now);
+ return false;
+ } else {
+ int count = getCount() + 1;
+ setCount(count);
+ EventLog.writeEvent(LOG_TAG_RESCUE_NOTE, Process.ROOT_UID, count, window);
+ if (Flags.recoverabilityDetection()) {
+ // After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply
+ // mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT.
+ return (count >= mBootTriggerCount)
+ || (performedMitigationsDuringWindow() && count > 1);
+ }
+ return count >= mBootTriggerCount;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean performedMitigationsDuringWindow() {
+ for (ObserverInternal observerInternal: mAllObservers.values()) {
+ if (observerInternal.getBootMitigationCount() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @GuardedBy("mLock")
+ private void resetAllObserversBootMitigationCount() {
+ for (int i = 0; i < mAllObservers.size(); i++) {
+ final ObserverInternal observer = mAllObservers.valueAt(i);
+ observer.setBootMitigationCount(0);
+ }
+ saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
+ }
+
+ @GuardedBy("mLock")
+ @SuppressWarnings("GuardedBy")
+ void readAllObserversBootMitigationCountIfNecessary(String filePath) {
+ File metadataFile = new File(filePath);
+ if (metadataFile.exists()) {
+ try {
+ FileInputStream fileStream = new FileInputStream(metadataFile);
+ ObjectInputStream objectStream = new ObjectInputStream(fileStream);
+ HashMap<String, Integer> bootMitigationCounts =
+ (HashMap<String, Integer>) objectStream.readObject();
+ objectStream.close();
+ fileStream.close();
+
+ for (int i = 0; i < mAllObservers.size(); i++) {
+ final ObserverInternal observer = mAllObservers.valueAt(i);
+ if (bootMitigationCounts.containsKey(observer.name)) {
+ observer.setBootMitigationCount(
+ bootMitigationCounts.get(observer.name));
+ }
+ }
+ } catch (Exception e) {
+ Slog.i(TAG, "Could not read observer metadata file: " + e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Register broadcast receiver for shutdown.
+ * We would save the observer state to persist across boots.
+ *
+ * @hide
+ */
+ public void registerShutdownBroadcastReceiver() {
+ BroadcastReceiver shutdownEventReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Only write if intent is relevant to device reboot or shutdown.
+ String intentAction = intent.getAction();
+ if (ACTION_REBOOT.equals(intentAction)
+ || ACTION_SHUTDOWN.equals(intentAction)) {
+ writeNow();
+ }
+ }
+ };
+
+ // Setup receiver for device reboots or shutdowns.
+ IntentFilter filter = new IntentFilter(ACTION_REBOOT);
+ filter.addAction(ACTION_SHUTDOWN);
+ mContext.registerReceiverForAllUsers(shutdownEventReceiver, filter, null,
+ /* run on main thread */ null);
+ }
+}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java
new file mode 100644
index 0000000..f1b2f6b
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java
@@ -0,0 +1,990 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
+import android.os.Build;
+import android.os.Environment;
+import android.os.PowerManager;
+import android.os.RecoverySystem;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.sysprop.CrashRecoveryProperties;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.ArrayUtils;
+import android.util.EventLog;
+import android.util.FileUtils;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.PackageWatchdog.FailureReasons;
+import com.android.server.PackageWatchdog.PackageHealthObserver;
+import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
+import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
+
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities to help rescue the system from crash loops. Callers are expected to
+ * report boot events and persistent app crashes, and if they happen frequently
+ * enough this class will slowly escalate through several rescue operations
+ * before finally rebooting and prompting the user if they want to wipe data as
+ * a last resort.
+ *
+ * @hide
+ */
+public class RescueParty {
+ @VisibleForTesting
+ static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue";
+ @VisibleForTesting
+ static final int LEVEL_NONE = 0;
+ @VisibleForTesting
+ static final int LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1;
+ @VisibleForTesting
+ static final int LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2;
+ @VisibleForTesting
+ static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3;
+ @VisibleForTesting
+ static final int LEVEL_WARM_REBOOT = 4;
+ @VisibleForTesting
+ static final int LEVEL_FACTORY_RESET = 5;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_NONE = 0;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_WARM_REBOOT = 3;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_FACTORY_RESET = 7;
+
+ @IntDef(prefix = { "RESCUE_LEVEL_" }, value = {
+ RESCUE_LEVEL_NONE,
+ RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET,
+ RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET,
+ RESCUE_LEVEL_WARM_REBOOT,
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
+ RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
+ RESCUE_LEVEL_FACTORY_RESET
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RescueLevels {}
+
+ @VisibleForTesting
+ static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit";
+ @VisibleForTesting
+ static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1;
+ @VisibleForTesting
+ static final String TAG = "RescueParty";
+ @VisibleForTesting
+ static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2);
+ @VisibleForTesting
+ static final int DEVICE_CONFIG_RESET_MODE = Settings.RESET_MODE_TRUSTED_DEFAULTS;
+ // The DeviceConfig namespace containing all RescueParty switches.
+ @VisibleForTesting
+ static final String NAMESPACE_CONFIGURATION = "configuration";
+ @VisibleForTesting
+ static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG =
+ "namespace_to_package_mapping";
+ @VisibleForTesting
+ static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440;
+
+ private static final String NAME = "rescue-party-observer";
+
+ private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue";
+ private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device";
+ private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG =
+ "persist.device_config.configuration.disable_rescue_party";
+ private static final String PROP_DISABLE_FACTORY_RESET_FLAG =
+ "persist.device_config.configuration.disable_rescue_party_factory_reset";
+ private static final String PROP_THROTTLE_DURATION_MIN_FLAG =
+ "persist.device_config.configuration.rescue_party_throttle_duration_min";
+
+ private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
+ | ApplicationInfo.FLAG_SYSTEM;
+
+ /**
+ * EventLog tags used when logging into the event log. Note the values must be sync with
+ * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct
+ * name translation.
+ */
+ private static final int LOG_TAG_RESCUE_SUCCESS = 2902;
+ private static final int LOG_TAG_RESCUE_FAILURE = 2903;
+
+ /** Register the Rescue Party observer as a Package Watchdog health observer */
+ public static void registerHealthObserver(Context context) {
+ PackageWatchdog.getInstance(context).registerHealthObserver(
+ RescuePartyObserver.getInstance(context));
+ }
+
+ private static boolean isDisabled() {
+ // Check if we're explicitly enabled for testing
+ if (SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) {
+ return false;
+ }
+
+ // We're disabled if the DeviceConfig disable flag is set to true.
+ // This is in case that an emergency rollback of the feature is needed.
+ if (SystemProperties.getBoolean(PROP_DEVICE_CONFIG_DISABLE_FLAG, false)) {
+ Slog.v(TAG, "Disabled because of DeviceConfig flag");
+ return true;
+ }
+
+ // We're disabled on all engineering devices
+ if (Build.TYPE.equals("eng")) {
+ Slog.v(TAG, "Disabled because of eng build");
+ return true;
+ }
+
+ // We're disabled on userdebug devices connected over USB, since that's
+ // a decent signal that someone is actively trying to debug the device,
+ // or that it's in a lab environment.
+ if (Build.TYPE.equals("userdebug") && isUsbActive()) {
+ Slog.v(TAG, "Disabled because of active USB connection");
+ return true;
+ }
+
+ // One last-ditch check
+ if (SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) {
+ Slog.v(TAG, "Disabled because of manual property");
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if we're currently attempting to reboot for a factory reset. This method must
+ * return true if RescueParty tries to reboot early during a boot loop, since the device
+ * will not be fully booted at this time.
+ */
+ public static boolean isRecoveryTriggeredReboot() {
+ return isFactoryResetPropertySet() || isRebootPropertySet();
+ }
+
+ static boolean isFactoryResetPropertySet() {
+ return CrashRecoveryProperties.attemptingFactoryReset().orElse(false);
+ }
+
+ static boolean isRebootPropertySet() {
+ return CrashRecoveryProperties.attemptingReboot().orElse(false);
+ }
+
+ protected static long getLastFactoryResetTimeMs() {
+ return CrashRecoveryProperties.lastFactoryResetTimeMs().orElse(0L);
+ }
+
+ protected static int getMaxRescueLevelAttempted() {
+ return CrashRecoveryProperties.maxRescueLevelAttempted().orElse(LEVEL_NONE);
+ }
+
+ protected static void setFactoryResetProperty(boolean value) {
+ CrashRecoveryProperties.attemptingFactoryReset(value);
+ }
+ protected static void setRebootProperty(boolean value) {
+ CrashRecoveryProperties.attemptingReboot(value);
+ }
+
+ protected static void setLastFactoryResetTimeMs(long value) {
+ CrashRecoveryProperties.lastFactoryResetTimeMs(value);
+ }
+
+ protected static void setMaxRescueLevelAttempted(int level) {
+ CrashRecoveryProperties.maxRescueLevelAttempted(level);
+ }
+
+ private static Set<String> getPresetNamespacesForPackages(List<String> packageNames) {
+ Set<String> resultSet = new ArraySet<String>();
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ try {
+ String flagVal = DeviceConfig.getString(NAMESPACE_CONFIGURATION,
+ NAMESPACE_TO_PACKAGE_MAPPING_FLAG, "");
+ String[] mappingEntries = flagVal.split(",");
+ for (int i = 0; i < mappingEntries.length; i++) {
+ if (TextUtils.isEmpty(mappingEntries[i])) {
+ continue;
+ }
+ String[] splitEntry = mappingEntries[i].split(":");
+ if (splitEntry.length != 2) {
+ throw new RuntimeException("Invalid mapping entry: " + mappingEntries[i]);
+ }
+ String namespace = splitEntry[0];
+ String packageName = splitEntry[1];
+
+ if (packageNames.contains(packageName)) {
+ resultSet.add(namespace);
+ }
+ }
+ } catch (Exception e) {
+ resultSet.clear();
+ Slog.e(TAG, "Failed to read preset package to namespaces mapping.", e);
+ } finally {
+ return resultSet;
+ }
+ } else {
+ return resultSet;
+ }
+ }
+
+ @VisibleForTesting
+ static long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ private static class RescuePartyMonitorCallback implements DeviceConfig.MonitorCallback {
+ Context mContext;
+
+ RescuePartyMonitorCallback(Context context) {
+ this.mContext = context;
+ }
+
+ public void onNamespaceUpdate(@NonNull String updatedNamespace) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ startObservingPackages(mContext, updatedNamespace);
+ }
+ }
+
+ public void onDeviceConfigAccess(@NonNull String callingPackage,
+ @NonNull String namespace) {
+
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ RescuePartyObserver.getInstance(mContext).recordDeviceConfigAccess(
+ callingPackage,
+ namespace);
+ }
+ }
+ }
+
+ private static void startObservingPackages(Context context, @NonNull String updatedNamespace) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ RescuePartyObserver rescuePartyObserver = RescuePartyObserver.getInstance(context);
+ Set<String> callingPackages = rescuePartyObserver.getCallingPackagesSet(
+ updatedNamespace);
+ if (callingPackages == null) {
+ return;
+ }
+ List<String> callingPackageList = new ArrayList<>();
+ callingPackageList.addAll(callingPackages);
+ Slog.i(TAG, "Starting to observe: " + callingPackageList + ", updated namespace: "
+ + updatedNamespace);
+ PackageWatchdog.getInstance(context).startObservingHealth(
+ rescuePartyObserver,
+ callingPackageList,
+ DEFAULT_OBSERVING_DURATION_MS);
+ }
+ }
+
+ private static int getMaxRescueLevel(boolean mayPerformReboot) {
+ if (Flags.recoverabilityDetection()) {
+ if (!mayPerformReboot
+ || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+ return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT,
+ DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT);
+ }
+ return RESCUE_LEVEL_FACTORY_RESET;
+ } else {
+ if (!mayPerformReboot
+ || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+ return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+ }
+ return LEVEL_FACTORY_RESET;
+ }
+ }
+
+ private static int getMaxRescueLevel() {
+ if (!SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+ return Level.factoryReset();
+ }
+ return Level.reboot();
+ }
+
+ /**
+ * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
+ *
+ * @param mitigationCount: the mitigation attempt number (1 = first attempt etc.)
+ * @param mayPerformReboot: whether or not a reboot and factory reset may be performed
+ * for the given failure.
+ * @return the rescue level for the n-th mitigation attempt.
+ */
+ private static int getRescueLevel(int mitigationCount, boolean mayPerformReboot) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ if (mitigationCount == 1) {
+ return LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS;
+ } else if (mitigationCount == 2) {
+ return LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES;
+ } else if (mitigationCount == 3) {
+ return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+ } else if (mitigationCount == 4) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_WARM_REBOOT);
+ } else if (mitigationCount >= 5) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_FACTORY_RESET);
+ } else {
+ Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount);
+ return LEVEL_NONE;
+ }
+ } else {
+ if (mitigationCount == 1) {
+ return Level.reboot();
+ } else if (mitigationCount >= 2) {
+ return Math.min(getMaxRescueLevel(), Level.factoryReset());
+ } else {
+ Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount);
+ return LEVEL_NONE;
+ }
+ }
+ }
+
+ /**
+ * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
+ * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and
+ * all device config reset). Behaves as if one mitigation attempt was already done.
+ *
+ * @param mitigationCount the mitigation attempt number (1 = first attempt etc.).
+ * @param mayPerformReboot whether or not a reboot and factory reset may be performed
+ * for the given failure.
+ * @param failedPackage in case of bootloop this is null.
+ * @return the rescue level for the n-th mitigation attempt.
+ */
+ private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot,
+ @Nullable VersionedPackage failedPackage) {
+ // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed
+ // package.
+ if (failedPackage == null && mitigationCount > 0) {
+ mitigationCount += 1;
+ }
+ if (mitigationCount == 1) {
+ return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET;
+ } else if (mitigationCount == 2) {
+ return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET;
+ } else if (mitigationCount == 3) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT);
+ } else if (mitigationCount == 4) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS);
+ } else if (mitigationCount == 5) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES);
+ } else if (mitigationCount == 6) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS);
+ } else if (mitigationCount >= 7) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET);
+ } else {
+ return RESCUE_LEVEL_NONE;
+ }
+ }
+
+ /**
+ * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
+ *
+ * @param mitigationCount the mitigation attempt number (1 = first attempt etc.).
+ * @return the rescue level for the n-th mitigation attempt.
+ */
+ private static @RescueLevels int getRescueLevel(int mitigationCount) {
+ if (mitigationCount == 1) {
+ return Level.reboot();
+ } else if (mitigationCount >= 2) {
+ return Math.min(getMaxRescueLevel(), Level.factoryReset());
+ } else {
+ return Level.none();
+ }
+ }
+
+ private static void executeRescueLevel(Context context, @Nullable String failedPackage,
+ int level) {
+ Slog.w(TAG, "Attempting rescue level " + levelToString(level));
+ try {
+ executeRescueLevelInternal(context, level, failedPackage);
+ EventLog.writeEvent(LOG_TAG_RESCUE_SUCCESS, level);
+ String successMsg = "Finished rescue level " + levelToString(level);
+ if (!TextUtils.isEmpty(failedPackage)) {
+ successMsg += " for package " + failedPackage;
+ }
+ logCrashRecoveryEvent(Log.DEBUG, successMsg);
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ }
+
+ private static void executeRescueLevelInternal(Context context, int level, @Nullable
+ String failedPackage) throws Exception {
+ if (Flags.recoverabilityDetection()) {
+ executeRescueLevelInternalNew(context, level, failedPackage);
+ } else {
+ executeRescueLevelInternalOld(context, level, failedPackage);
+ }
+ }
+
+ private static void executeRescueLevelInternalOld(Context context, int level, @Nullable
+ String failedPackage) throws Exception {
+ CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
+ level, levelToString(level));
+ // Try our best to reset all settings possible, and once finished
+ // rethrow any exception that we encountered
+ Exception res = null;
+ switch (level) {
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ break;
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ break;
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ break;
+ case LEVEL_WARM_REBOOT:
+ executeWarmReboot(context, level, failedPackage);
+ break;
+ case LEVEL_FACTORY_RESET:
+ // Before the completion of Reboot, if any crash happens then PackageWatchdog
+ // escalates to next level i.e. factory reset, as they happen in separate threads.
+ // Adding a check to prevent factory reset to execute before above reboot completes.
+ // Note: this reboot property is not persistent resets after reboot is completed.
+ if (isRebootPropertySet()) {
+ return;
+ }
+ executeFactoryReset(context, level, failedPackage);
+ break;
+ }
+
+ if (res != null) {
+ throw res;
+ }
+ }
+
+ private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level,
+ @Nullable String failedPackage) throws Exception {
+ CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
+ level, levelToString(level));
+ switch (level) {
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ break;
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ break;
+ case RESCUE_LEVEL_WARM_REBOOT:
+ executeWarmReboot(context, level, failedPackage);
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ // do nothing
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ // do nothing
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ // do nothing
+ break;
+ case RESCUE_LEVEL_FACTORY_RESET:
+ // Before the completion of Reboot, if any crash happens then PackageWatchdog
+ // escalates to next level i.e. factory reset, as they happen in separate threads.
+ // Adding a check to prevent factory reset to execute before above reboot completes.
+ // Note: this reboot property is not persistent resets after reboot is completed.
+ if (isRebootPropertySet()) {
+ return;
+ }
+ executeFactoryReset(context, level, failedPackage);
+ break;
+ }
+ }
+
+ private static void executeWarmReboot(Context context, int level,
+ @Nullable String failedPackage) {
+ if (Flags.deprecateFlagsAndSettingsResets()) {
+ if (shouldThrottleReboot()) {
+ return;
+ }
+ }
+
+ // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
+ // when device shutting down.
+ setRebootProperty(true);
+
+ if (Flags.synchronousRebootInRescueParty()) {
+ try {
+ PowerManager pm = context.getSystemService(PowerManager.class);
+ if (pm != null) {
+ pm.reboot(TAG);
+ }
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ } else {
+ Runnable runnable = () -> {
+ try {
+ PowerManager pm = context.getSystemService(PowerManager.class);
+ if (pm != null) {
+ pm.reboot(TAG);
+ }
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ }
+ }
+
+ private static void executeFactoryReset(Context context, int level,
+ @Nullable String failedPackage) {
+ if (Flags.deprecateFlagsAndSettingsResets()) {
+ if (shouldThrottleReboot()) {
+ return;
+ }
+ }
+ setFactoryResetProperty(true);
+ long now = System.currentTimeMillis();
+ setLastFactoryResetTimeMs(now);
+
+ if (Flags.synchronousRebootInRescueParty()) {
+ try {
+ RecoverySystem.rebootPromptAndWipeUserData(context, TAG + "," + failedPackage);
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ } else {
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ RecoverySystem.rebootPromptAndWipeUserData(context,
+ TAG + "," + failedPackage);
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ }
+ }
+
+
+ private static String getCompleteMessage(Throwable t) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(t.getMessage());
+ while ((t = t.getCause()) != null) {
+ builder.append(": ").append(t.getMessage());
+ }
+ return builder.toString();
+ }
+
+ private static void logRescueException(int level, @Nullable String failedPackageName,
+ Throwable t) {
+ final String msg = getCompleteMessage(t);
+ EventLog.writeEvent(LOG_TAG_RESCUE_FAILURE, level, msg);
+ String failureMsg = "Failed rescue level " + levelToString(level);
+ if (!TextUtils.isEmpty(failedPackageName)) {
+ failureMsg += " for package " + failedPackageName;
+ }
+ logCrashRecoveryEvent(Log.ERROR, failureMsg + ": " + msg);
+ }
+
+ private static int mapRescueLevelToUserImpact(int rescueLevel) {
+ if (Flags.recoverabilityDetection()) {
+ switch (rescueLevel) {
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_40;
+ case RESCUE_LEVEL_WARM_REBOOT:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75;
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80;
+ case RESCUE_LEVEL_FACTORY_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+ default:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ } else {
+ switch (rescueLevel) {
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ case LEVEL_WARM_REBOOT:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+ case LEVEL_FACTORY_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+ default:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ }
+ }
+
+ /**
+ * Handle mitigation action for package failures. This observer will be register to Package
+ * Watchdog and will receive calls about package failures. This observer is persistent so it
+ * may choose to mitigate failures for packages it has not explicitly asked to observe.
+ */
+ public static class RescuePartyObserver implements PackageHealthObserver {
+
+ private final Context mContext;
+ private final Map<String, Set<String>> mCallingPackageNamespaceSetMap = new HashMap<>();
+ private final Map<String, Set<String>> mNamespaceCallingPackageSetMap = new HashMap<>();
+
+ @GuardedBy("RescuePartyObserver.class")
+ static RescuePartyObserver sRescuePartyObserver;
+
+ private RescuePartyObserver(Context context) {
+ mContext = context;
+ }
+
+ /** Creates or gets singleton instance of RescueParty. */
+ public static RescuePartyObserver getInstance(Context context) {
+ synchronized (RescuePartyObserver.class) {
+ if (sRescuePartyObserver == null) {
+ sRescuePartyObserver = new RescuePartyObserver(context);
+ }
+ return sRescuePartyObserver;
+ }
+ }
+
+ /** Gets singleton instance. It returns null if the instance is not created yet.*/
+ @Nullable
+ public static RescuePartyObserver getInstanceIfCreated() {
+ synchronized (RescuePartyObserver.class) {
+ return sRescuePartyObserver;
+ }
+ }
+
+ @VisibleForTesting
+ static void reset() {
+ synchronized (RescuePartyObserver.class) {
+ sRescuePartyObserver = null;
+ }
+ }
+
+ @Override
+ public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage,
+ @FailureReasons int failureReason, int mitigationCount) {
+ int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
+ || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) {
+ if (Flags.recoverabilityDetection()) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage), failedPackage));
+ } else {
+ impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount));
+ }
+ } else {
+ impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage)));
+ }
+ }
+
+ Slog.i(TAG, "Checking available remediations for health check failure."
+ + " failedPackage: "
+ + (failedPackage == null ? null : failedPackage.getPackageName())
+ + " failureReason: " + failureReason
+ + " available impact: " + impact);
+ return impact;
+ }
+
+ @Override
+ public boolean execute(@Nullable VersionedPackage failedPackage,
+ @FailureReasons int failureReason, int mitigationCount) {
+ if (isDisabled()) {
+ return false;
+ }
+ Slog.i(TAG, "Executing remediation."
+ + " failedPackage: "
+ + (failedPackage == null ? null : failedPackage.getPackageName())
+ + " failureReason: " + failureReason
+ + " mitigationCount: " + mitigationCount);
+ if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
+ || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) {
+ final int level;
+ if (Flags.recoverabilityDetection()) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage),
+ failedPackage);
+ } else {
+ level = getRescueLevel(mitigationCount);
+ }
+ } else {
+ level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage));
+ }
+ executeRescueLevel(mContext,
+ failedPackage == null ? null : failedPackage.getPackageName(), level);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isPersistent() {
+ return true;
+ }
+
+ @Override
+ public boolean mayObservePackage(String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ // A package is a module if this is non-null
+ if (pm.getModuleInfo(packageName, 0) != null) {
+ return true;
+ }
+ } catch (PackageManager.NameNotFoundException | IllegalStateException ignore) {
+ }
+
+ return isPersistentSystemApp(packageName);
+ }
+
+ @Override
+ public int onBootLoop(int mitigationCount) {
+ if (isDisabled()) {
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ if (Flags.recoverabilityDetection()) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ true, /*failedPackage=*/ null));
+ } else {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount));
+ }
+ } else {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true));
+ }
+ }
+
+ @Override
+ public boolean executeBootLoopMitigation(int mitigationCount) {
+ if (isDisabled()) {
+ return false;
+ }
+ boolean mayPerformReboot = !shouldThrottleReboot();
+ final int level;
+ if (Flags.recoverabilityDetection()) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ level = getRescueLevel(mitigationCount, mayPerformReboot,
+ /*failedPackage=*/ null);
+ } else {
+ level = getRescueLevel(mitigationCount);
+ }
+ } else {
+ level = getRescueLevel(mitigationCount, mayPerformReboot);
+ }
+ executeRescueLevel(mContext, /*failedPackage=*/ null, level);
+ return true;
+ }
+
+ @Override
+ public String getUniqueIdentifier() {
+ return NAME;
+ }
+
+ /**
+ * Returns {@code true} if the failing package is non-null and performing a reboot or
+ * prompting a factory reset is an acceptable mitigation strategy for the package's
+ * failure, {@code false} otherwise.
+ */
+ private boolean mayPerformReboot(@Nullable VersionedPackage failingPackage) {
+ if (failingPackage == null) {
+ return false;
+ }
+ if (shouldThrottleReboot()) {
+ return false;
+ }
+
+ return isPersistentSystemApp(failingPackage.getPackageName());
+ }
+
+ private boolean isPersistentSystemApp(@NonNull String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private synchronized void recordDeviceConfigAccess(@NonNull String callingPackage,
+ @NonNull String namespace) {
+ if (!Flags.deprecateFlagsAndSettingsResets()) {
+ // Record it in calling packages to namespace map
+ Set<String> namespaceSet = mCallingPackageNamespaceSetMap.get(callingPackage);
+ if (namespaceSet == null) {
+ namespaceSet = new ArraySet<>();
+ mCallingPackageNamespaceSetMap.put(callingPackage, namespaceSet);
+ }
+ namespaceSet.add(namespace);
+ // Record it in namespace to calling packages map
+ Set<String> callingPackageSet = mNamespaceCallingPackageSetMap.get(namespace);
+ if (callingPackageSet == null) {
+ callingPackageSet = new ArraySet<>();
+ }
+ callingPackageSet.add(callingPackage);
+ mNamespaceCallingPackageSetMap.put(namespace, callingPackageSet);
+ }
+ }
+
+ private synchronized Set<String> getAffectedNamespaceSet(String failedPackage) {
+ return mCallingPackageNamespaceSetMap.get(failedPackage);
+ }
+
+ private synchronized Set<String> getAllAffectedNamespaceSet() {
+ return new HashSet<String>(mNamespaceCallingPackageSetMap.keySet());
+ }
+
+ private synchronized Set<String> getCallingPackagesSet(String namespace) {
+ return mNamespaceCallingPackageSetMap.get(namespace);
+ }
+ }
+
+ /**
+ * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset.
+ * Will return {@code false} if a factory reset was already offered recently.
+ */
+ private static boolean shouldThrottleReboot() {
+ Long lastResetTime = getLastFactoryResetTimeMs();
+ long now = System.currentTimeMillis();
+ long throttleDurationMin = SystemProperties.getLong(PROP_THROTTLE_DURATION_MIN_FLAG,
+ DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN);
+ return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin);
+ }
+
+ private static int[] getAllUserIds() {
+ int systemUserId = UserHandle.SYSTEM.getIdentifier();
+ int[] userIds = { systemUserId };
+ try {
+ for (File file : FileUtils.listFilesOrEmpty(Environment.getDataSystemDeDirectory())) {
+ try {
+ final int userId = Integer.parseInt(file.getName());
+ if (userId != systemUserId) {
+ userIds = ArrayUtils.appendInt(userIds, userId);
+ }
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ } catch (Throwable t) {
+ Slog.w(TAG, "Trouble discovering users", t);
+ }
+ return userIds;
+ }
+
+ /**
+ * Hacky test to check if the device has an active USB connection, which is
+ * a good proxy for someone doing local development work.
+ */
+ private static boolean isUsbActive() {
+ if (SystemProperties.getBoolean(PROP_VIRTUAL_DEVICE, false)) {
+ Slog.v(TAG, "Assuming virtual device is connected over USB");
+ return true;
+ }
+ try {
+ final String state = FileUtils
+ .readTextFile(new File("/sys/class/android_usb/android0/state"), 128, "");
+ return "CONFIGURED".equals(state.trim());
+ } catch (Throwable t) {
+ Slog.w(TAG, "Failed to determine if device was on USB", t);
+ return false;
+ }
+ }
+
+ private static class Level {
+ static int none() {
+ return Flags.recoverabilityDetection() ? RESCUE_LEVEL_NONE : LEVEL_NONE;
+ }
+
+ static int reboot() {
+ return Flags.recoverabilityDetection() ? RESCUE_LEVEL_WARM_REBOOT : LEVEL_WARM_REBOOT;
+ }
+
+ static int factoryReset() {
+ return Flags.recoverabilityDetection()
+ ? RESCUE_LEVEL_FACTORY_RESET
+ : LEVEL_FACTORY_RESET;
+ }
+ }
+
+ private static String levelToString(int level) {
+ if (Flags.recoverabilityDetection()) {
+ switch (level) {
+ case RESCUE_LEVEL_NONE:
+ return "NONE";
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ return "SCOPED_DEVICE_CONFIG_RESET";
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ return "ALL_DEVICE_CONFIG_RESET";
+ case RESCUE_LEVEL_WARM_REBOOT:
+ return "WARM_REBOOT";
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+ case RESCUE_LEVEL_FACTORY_RESET:
+ return "FACTORY_RESET";
+ default:
+ return Integer.toString(level);
+ }
+ } else {
+ switch (level) {
+ case LEVEL_NONE:
+ return "NONE";
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+ case LEVEL_WARM_REBOOT:
+ return "WARM_REBOOT";
+ case LEVEL_FACTORY_RESET:
+ return "FACTORY_RESET";
+ default:
+ return Integer.toString(level);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java
similarity index 100%
rename from services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
rename to packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
similarity index 100%
rename from services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
rename to packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java
new file mode 100644
index 0000000..2931652
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.rollback;
+
+import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.VersionedPackage;
+import android.content.rollback.PackageRollbackInfo;
+import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
+import android.crashrecovery.flags.Flags;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.PowerManager;
+import android.os.SystemProperties;
+import android.sysprop.CrashRecoveryProperties;
+import android.util.ArraySet;
+import android.util.FileUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.PackageWatchdog;
+import com.android.server.PackageWatchdog.FailureReasons;
+import com.android.server.PackageWatchdog.PackageHealthObserver;
+import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
+import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * {@link PackageHealthObserver} for {@link RollbackManagerService}.
+ * This class monitors crashes and triggers RollbackManager rollback accordingly.
+ * It also monitors native crashes for some short while after boot.
+ *
+ * @hide
+ */
+public final class RollbackPackageHealthObserver implements PackageHealthObserver {
+ private static final String TAG = "RollbackPackageHealthObserver";
+ private static final String NAME = "rollback-observer";
+ private static final String CLASS_NAME = RollbackPackageHealthObserver.class.getName();
+
+ private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
+ | ApplicationInfo.FLAG_SYSTEM;
+
+ private static final String PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG =
+ "persist.device_config.configuration.disable_high_impact_rollback";
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final File mLastStagedRollbackIdsFile;
+ private final File mTwoPhaseRollbackEnabledFile;
+ // Staged rollback ids that have been committed but their session is not yet ready
+ private final Set<Integer> mPendingStagedRollbackIds = new ArraySet<>();
+ // True if needing to roll back only rebootless apexes when native crash happens
+ private boolean mTwoPhaseRollbackEnabled;
+
+ @VisibleForTesting
+ public RollbackPackageHealthObserver(@NonNull Context context) {
+ mContext = context;
+ HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver");
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ File dataDir = new File(Environment.getDataDirectory(), "rollback-observer");
+ dataDir.mkdirs();
+ mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids");
+ mTwoPhaseRollbackEnabledFile = new File(dataDir, "two-phase-rollback-enabled");
+ PackageWatchdog.getInstance(mContext).registerHealthObserver(this);
+
+ if (SystemProperties.getBoolean("sys.boot_completed", false)) {
+ // Load the value from the file if system server has crashed and restarted
+ mTwoPhaseRollbackEnabled = readBoolean(mTwoPhaseRollbackEnabledFile);
+ } else {
+ // Disable two-phase rollback for a normal reboot. We assume the rebootless apex
+ // installed before reboot is stable if native crash didn't happen.
+ mTwoPhaseRollbackEnabled = false;
+ writeBoolean(mTwoPhaseRollbackEnabledFile, false);
+ }
+ }
+
+ @Override
+ public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage,
+ @FailureReasons int failureReason, int mitigationCount) {
+ int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ if (Flags.recoverabilityDetection()) {
+ List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+ List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
+ availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW);
+ if (!lowImpactRollbacks.isEmpty()) {
+ if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+ // For native crashes, we will directly roll back any available rollbacks at low
+ // impact level
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+ } else if (getRollbackForPackage(failedPackage, lowImpactRollbacks) != null) {
+ // Rollback is available for crashing low impact package
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+ } else {
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
+ }
+ }
+ } else {
+ boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class)
+ .getAvailableRollbacks().isEmpty();
+
+ if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH
+ && anyRollbackAvailable) {
+ // For native crashes, we will directly roll back any available rollbacks
+ // Note: For non-native crashes the rollback-all step has higher impact
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+ } else if (getAvailableRollback(failedPackage) != null) {
+ // Rollback is available, we may get a callback into #execute
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+ } else if (anyRollbackAvailable) {
+ // If any rollbacks are available, we will commit them
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
+ }
+ }
+
+ Slog.i(TAG, "Checking available remediations for health check failure."
+ + " failedPackage: "
+ + (failedPackage == null ? null : failedPackage.getPackageName())
+ + " failureReason: " + failureReason
+ + " available impact: " + impact);
+ return impact;
+ }
+
+ @Override
+ public boolean execute(@Nullable VersionedPackage failedPackage,
+ @FailureReasons int rollbackReason, int mitigationCount) {
+ Slog.i(TAG, "Executing remediation."
+ + " failedPackage: "
+ + (failedPackage == null ? null : failedPackage.getPackageName())
+ + " rollbackReason: " + rollbackReason
+ + " mitigationCount: " + mitigationCount);
+ if (Flags.recoverabilityDetection()) {
+ List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+ if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+ mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
+ return true;
+ }
+
+ List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
+ availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW);
+ RollbackInfo rollback = getRollbackForPackage(failedPackage, lowImpactRollbacks);
+ if (rollback != null) {
+ mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason));
+ } else if (!lowImpactRollbacks.isEmpty()) {
+ // Apply all available low impact rollbacks.
+ mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
+ }
+ } else {
+ if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+ mHandler.post(() -> rollbackAll(rollbackReason));
+ return true;
+ }
+
+ RollbackInfo rollback = getAvailableRollback(failedPackage);
+ if (rollback != null) {
+ mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason));
+ } else {
+ mHandler.post(() -> rollbackAll(rollbackReason));
+ }
+ }
+
+ // Assume rollbacks executed successfully
+ return true;
+ }
+
+ @Override
+ public int onBootLoop(int mitigationCount) {
+ int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ if (Flags.recoverabilityDetection()) {
+ List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+ if (!availableRollbacks.isEmpty()) {
+ impact = getUserImpactBasedOnRollbackImpactLevel(availableRollbacks);
+ }
+ }
+ return impact;
+ }
+
+ @Override
+ public boolean executeBootLoopMitigation(int mitigationCount) {
+ if (Flags.recoverabilityDetection()) {
+ List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+
+ triggerLeastImpactLevelRollback(availableRollbacks,
+ PackageWatchdog.FAILURE_REASON_BOOT_LOOP);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ @NonNull
+ public String getUniqueIdentifier() {
+ return NAME;
+ }
+
+ @Override
+ public boolean isPersistent() {
+ return true;
+ }
+
+ @Override
+ public boolean mayObservePackage(@NonNull String packageName) {
+ if (getAvailableRollbacks().isEmpty()) {
+ return false;
+ }
+ return isPersistentSystemApp(packageName);
+ }
+
+ private List<RollbackInfo> getAvailableRollbacks() {
+ return mContext.getSystemService(RollbackManager.class).getAvailableRollbacks();
+ }
+
+ private boolean isPersistentSystemApp(@NonNull String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private void assertInWorkerThread() {
+ Preconditions.checkState(mHandler.getLooper().isCurrentThread());
+ }
+
+ /**
+ * Start observing health of {@code packages} for {@code durationMs}.
+ * This may cause {@code packages} to be rolled back if they crash too freqeuntly.
+ */
+ @AnyThread
+ @NonNull
+ public void startObservingHealth(@NonNull List<String> packages, @NonNull long durationMs) {
+ PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs);
+ }
+
+ @AnyThread
+ @NonNull
+ public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) {
+ mHandler.post(() -> {
+ // Enable two-phase rollback when a rebootless apex rollback is made available.
+ // We assume the rebootless apex is stable and is less likely to be the cause
+ // if native crash doesn't happen before reboot. So we will clear the flag and disable
+ // two-phase rollback after reboot.
+ if (isRebootlessApex(rollback)) {
+ mTwoPhaseRollbackEnabled = true;
+ writeBoolean(mTwoPhaseRollbackEnabledFile, true);
+ }
+ });
+ }
+
+ private static boolean isRebootlessApex(RollbackInfo rollback) {
+ if (!rollback.isStaged()) {
+ for (PackageRollbackInfo info : rollback.getPackages()) {
+ if (info.isApex()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Verifies the rollback state after a reboot and schedules polling for sometime after reboot
+ * to check for native crashes and mitigate them if needed.
+ */
+ @AnyThread
+ public void onBootCompletedAsync() {
+ mHandler.post(()->onBootCompleted());
+ }
+
+ @WorkerThread
+ private void onBootCompleted() {
+ assertInWorkerThread();
+
+ RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
+ if (!rollbackManager.getAvailableRollbacks().isEmpty()) {
+ // TODO(gavincorkery): Call into Package Watchdog from outside the observer
+ PackageWatchdog.getInstance(mContext).scheduleCheckAndMitigateNativeCrashes();
+ }
+
+ SparseArray<String> rollbackIds = popLastStagedRollbackIds();
+ for (int i = 0; i < rollbackIds.size(); i++) {
+ WatchdogRollbackLogger.logRollbackStatusOnBoot(mContext,
+ rollbackIds.keyAt(i), rollbackIds.valueAt(i),
+ rollbackManager.getRecentlyCommittedRollbacks());
+ }
+ }
+
+ @AnyThread
+ private RollbackInfo getAvailableRollback(VersionedPackage failedPackage) {
+ RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
+ for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) {
+ for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
+ if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) {
+ return rollback;
+ }
+ // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have
+ // to rely on complicated reasoning as below
+
+ // Due to b/147666157, for apk in apex, we do not know the version we are rolling
+ // back from. But if a package X is embedded in apex A exclusively (not embedded in
+ // any other apex), which is not guaranteed, then it is sufficient to check only
+ // package names here, as the version of failedPackage and the PackageRollbackInfo
+ // can't be different. If failedPackage has a higher version, then it must have
+ // been updated somehow. There are two ways: it was updated by an update of apex A
+ // or updated directly as apk. In both cases, this rollback would have gotten
+ // expired when onPackageReplaced() was called. Since the rollback exists, it has
+ // same version as failedPackage.
+ if (packageRollback.isApkInApex()
+ && packageRollback.getVersionRolledBackFrom().getPackageName()
+ .equals(failedPackage.getPackageName())) {
+ return rollback;
+ }
+ }
+ }
+ return null;
+ }
+
+ @AnyThread
+ private RollbackInfo getRollbackForPackage(@Nullable VersionedPackage failedPackage,
+ List<RollbackInfo> availableRollbacks) {
+ if (failedPackage == null) {
+ return null;
+ }
+
+ for (RollbackInfo rollback : availableRollbacks) {
+ for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
+ if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) {
+ return rollback;
+ }
+ // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have
+ // to rely on complicated reasoning as below
+
+ // Due to b/147666157, for apk in apex, we do not know the version we are rolling
+ // back from. But if a package X is embedded in apex A exclusively (not embedded in
+ // any other apex), which is not guaranteed, then it is sufficient to check only
+ // package names here, as the version of failedPackage and the PackageRollbackInfo
+ // can't be different. If failedPackage has a higher version, then it must have
+ // been updated somehow. There are two ways: it was updated by an update of apex A
+ // or updated directly as apk. In both cases, this rollback would have gotten
+ // expired when onPackageReplaced() was called. Since the rollback exists, it has
+ // same version as failedPackage.
+ if (packageRollback.isApkInApex()
+ && packageRollback.getVersionRolledBackFrom().getPackageName()
+ .equals(failedPackage.getPackageName())) {
+ return rollback;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns {@code true} if staged session associated with {@code rollbackId} was marked
+ * as handled, {@code false} if already handled.
+ */
+ @WorkerThread
+ private boolean markStagedSessionHandled(int rollbackId) {
+ assertInWorkerThread();
+ return mPendingStagedRollbackIds.remove(rollbackId);
+ }
+
+ /**
+ * Returns {@code true} if all pending staged rollback sessions were marked as handled,
+ * {@code false} if there is any left.
+ */
+ @WorkerThread
+ private boolean isPendingStagedSessionsEmpty() {
+ assertInWorkerThread();
+ return mPendingStagedRollbackIds.isEmpty();
+ }
+
+ private static boolean readBoolean(File file) {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ return fis.read() == 1;
+ } catch (IOException ignore) {
+ return false;
+ }
+ }
+
+ private static void writeBoolean(File file, boolean value) {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(value ? 1 : 0);
+ fos.flush();
+ FileUtils.sync(fos);
+ } catch (IOException ignore) {
+ }
+ }
+
+ @WorkerThread
+ private void saveStagedRollbackId(int stagedRollbackId, @Nullable VersionedPackage logPackage) {
+ assertInWorkerThread();
+ writeStagedRollbackId(mLastStagedRollbackIdsFile, stagedRollbackId, logPackage);
+ }
+
+ static void writeStagedRollbackId(File file, int stagedRollbackId,
+ @Nullable VersionedPackage logPackage) {
+ try {
+ FileOutputStream fos = new FileOutputStream(file, true);
+ PrintWriter pw = new PrintWriter(fos);
+ String logPackageName = logPackage != null ? logPackage.getPackageName() : "";
+ pw.append(String.valueOf(stagedRollbackId)).append(",").append(logPackageName);
+ pw.println();
+ pw.flush();
+ FileUtils.sync(fos);
+ pw.close();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to save last staged rollback id", e);
+ file.delete();
+ }
+ }
+
+ @WorkerThread
+ private SparseArray<String> popLastStagedRollbackIds() {
+ assertInWorkerThread();
+ try {
+ return readStagedRollbackIds(mLastStagedRollbackIdsFile);
+ } finally {
+ mLastStagedRollbackIdsFile.delete();
+ }
+ }
+
+ static SparseArray<String> readStagedRollbackIds(File file) {
+ SparseArray<String> result = new SparseArray<>();
+ try {
+ String line;
+ BufferedReader reader = new BufferedReader(new FileReader(file));
+ while ((line = reader.readLine()) != null) {
+ // Each line is of the format: "id,logging_package"
+ String[] values = line.trim().split(",");
+ String rollbackId = values[0];
+ String logPackageName = "";
+ if (values.length > 1) {
+ logPackageName = values[1];
+ }
+ result.put(Integer.parseInt(rollbackId), logPackageName);
+ }
+ } catch (Exception ignore) {
+ return new SparseArray<>();
+ }
+ return result;
+ }
+
+
+ /**
+ * Returns true if the package name is the name of a module.
+ */
+ @AnyThread
+ private boolean isModule(String packageName) {
+ // Check if the package is listed among the system modules or is an
+ // APK inside an updatable APEX.
+ try {
+ final PackageInfo pkg = mContext.getPackageManager()
+ .getPackageInfo(packageName, 0 /* flags */);
+ String apexPackageName = pkg.getApexPackageName();
+ if (apexPackageName != null) {
+ packageName = apexPackageName;
+ }
+
+ return pm.getModuleInfo(packageName, 0 /* flags */) != null;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Rolls back the session that owns {@code failedPackage}
+ *
+ * @param rollback {@code rollbackInfo} of the {@code failedPackage}
+ * @param failedPackage the package that needs to be rolled back
+ */
+ @WorkerThread
+ private void rollbackPackage(RollbackInfo rollback, VersionedPackage failedPackage,
+ @FailureReasons int rollbackReason) {
+ assertInWorkerThread();
+ String failedPackageName = (failedPackage == null ? null : failedPackage.getPackageName());
+
+ Slog.i(TAG, "Rolling back package. RollbackId: " + rollback.getRollbackId()
+ + " failedPackage: " + failedPackageName
+ + " rollbackReason: " + rollbackReason);
+ logCrashRecoveryEvent(Log.DEBUG, String.format("Rolling back %s. Reason: %s",
+ failedPackageName, rollbackReason));
+ final RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
+ int reasonToLog = WatchdogRollbackLogger.mapFailureReasonToMetric(rollbackReason);
+ final String failedPackageToLog;
+ if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+ failedPackageToLog = SystemProperties.get(
+ "sys.init.updatable_crashing_process_name", "");
+ } else {
+ failedPackageToLog = failedPackage.getPackageName();
+ }
+ VersionedPackage logPackageTemp = null;
+ if (isModule(failedPackage.getPackageName())) {
+ logPackageTemp = WatchdogRollbackLogger.getLogPackage(mContext, failedPackage);
+ }
+
+ final VersionedPackage logPackage = logPackageTemp;
+ WatchdogRollbackLogger.logEvent(logPackage,
+ CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE,
+ reasonToLog, failedPackageToLog);
+
+ Consumer<Intent> onResult = result -> {
+ assertInWorkerThread();
+ int status = result.getIntExtra(RollbackManager.EXTRA_STATUS,
+ RollbackManager.STATUS_FAILURE);
+ if (status == RollbackManager.STATUS_SUCCESS) {
+ if (rollback.isStaged()) {
+ int rollbackId = rollback.getRollbackId();
+ saveStagedRollbackId(rollbackId, logPackage);
+ WatchdogRollbackLogger.logEvent(logPackage,
+ CrashRecoveryStatsLog
+ .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED,
+ reasonToLog, failedPackageToLog);
+
+ } else {
+ WatchdogRollbackLogger.logEvent(logPackage,
+ CrashRecoveryStatsLog
+ .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS,
+ reasonToLog, failedPackageToLog);
+ }
+ } else {
+ WatchdogRollbackLogger.logEvent(logPackage,
+ CrashRecoveryStatsLog
+ .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE,
+ reasonToLog, failedPackageToLog);
+ }
+ if (rollback.isStaged()) {
+ markStagedSessionHandled(rollback.getRollbackId());
+ // Wait for all pending staged sessions to get handled before rebooting.
+ if (isPendingStagedSessionsEmpty()) {
+ CrashRecoveryProperties.attemptingReboot(true);
+ mContext.getSystemService(PowerManager.class).reboot("Rollback staged install");
+ }
+ }
+ };
+
+ // Define a BroadcastReceiver to handle the result
+ BroadcastReceiver rollbackReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent result) {
+ mHandler.post(() -> onResult.accept(result));
+ }
+ };
+
+ String intentActionName = CLASS_NAME + rollback.getRollbackId();
+ // Register the BroadcastReceiver
+ mContext.registerReceiver(rollbackReceiver,
+ new IntentFilter(intentActionName),
+ Context.RECEIVER_NOT_EXPORTED);
+
+ Intent intentReceiver = new Intent(intentActionName);
+ intentReceiver.putExtra("rollbackId", rollback.getRollbackId());
+ intentReceiver.setPackage(mContext.getPackageName());
+ intentReceiver.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+
+ PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext,
+ rollback.getRollbackId(),
+ intentReceiver,
+ PendingIntent.FLAG_MUTABLE);
+
+ rollbackManager.commitRollback(rollback.getRollbackId(),
+ Collections.singletonList(failedPackage),
+ rollbackPendingIntent.getIntentSender());
+ }
+
+ /**
+ * Two-phase rollback:
+ * 1. roll back rebootless apexes first
+ * 2. roll back all remaining rollbacks if native crash doesn't stop after (1) is done
+ *
+ * This approach gives us a better chance to correctly attribute native crash to rebootless
+ * apex update without rolling back Mainline updates which might contains critical security
+ * fixes.
+ */
+ @WorkerThread
+ private boolean useTwoPhaseRollback(List<RollbackInfo> rollbacks) {
+ assertInWorkerThread();
+ if (!mTwoPhaseRollbackEnabled) {
+ return false;
+ }
+
+ Slog.i(TAG, "Rolling back all rebootless APEX rollbacks");
+ boolean found = false;
+ for (RollbackInfo rollback : rollbacks) {
+ if (isRebootlessApex(rollback)) {
+ VersionedPackage firstRollback =
+ rollback.getPackages().get(0).getVersionRolledBackFrom();
+ rollbackPackage(rollback, firstRollback,
+ PackageWatchdog.FAILURE_REASON_NATIVE_CRASH);
+ found = true;
+ }
+ }
+ return found;
+ }
+
+ /**
+ * Rollback the package that has minimum rollback impact level.
+ * @param availableRollbacks all available rollbacks
+ * @param rollbackReason reason to rollback
+ */
+ private void triggerLeastImpactLevelRollback(List<RollbackInfo> availableRollbacks,
+ @FailureReasons int rollbackReason) {
+ int minRollbackImpactLevel = getMinRollbackImpactLevel(availableRollbacks);
+
+ if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_LOW) {
+ // Apply all available low impact rollbacks.
+ mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
+ } else if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH) {
+ // Check disable_high_impact_rollback device config before performing rollback
+ if (SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) {
+ return;
+ }
+ // Rollback one package at a time. If that doesn't resolve the issue, rollback
+ // next with same impact level.
+ mHandler.post(() -> rollbackHighImpact(availableRollbacks, rollbackReason));
+ }
+ }
+
+ /**
+ * sort the available high impact rollbacks by first package name to have a deterministic order.
+ * Apply the first available rollback.
+ * @param availableRollbacks all available rollbacks
+ * @param rollbackReason reason to rollback
+ */
+ @WorkerThread
+ private void rollbackHighImpact(List<RollbackInfo> availableRollbacks,
+ @FailureReasons int rollbackReason) {
+ assertInWorkerThread();
+ List<RollbackInfo> highImpactRollbacks =
+ getRollbacksAvailableForImpactLevel(
+ availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+
+ // sort rollbacks based on package name of the first package. This is to have a
+ // deterministic order of rollbacks.
+ List<RollbackInfo> sortedHighImpactRollbacks = highImpactRollbacks.stream().sorted(
+ Comparator.comparing(a -> a.getPackages().get(0).getPackageName())).toList();
+ VersionedPackage firstRollback =
+ sortedHighImpactRollbacks
+ .get(0)
+ .getPackages()
+ .get(0)
+ .getVersionRolledBackFrom();
+ Slog.i(TAG, "Rolling back high impact rollback for package: "
+ + firstRollback.getPackageName());
+ rollbackPackage(sortedHighImpactRollbacks.get(0), firstRollback, rollbackReason);
+ }
+
+ @WorkerThread
+ private void rollbackAll(@FailureReasons int rollbackReason) {
+ assertInWorkerThread();
+ RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
+ List<RollbackInfo> rollbacks = rollbackManager.getAvailableRollbacks();
+ if (useTwoPhaseRollback(rollbacks)) {
+ return;
+ }
+
+ Slog.i(TAG, "Rolling back all available rollbacks");
+ // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all
+ // pending staged rollbacks are handled.
+ for (RollbackInfo rollback : rollbacks) {
+ if (rollback.isStaged()) {
+ mPendingStagedRollbackIds.add(rollback.getRollbackId());
+ }
+ }
+
+ for (RollbackInfo rollback : rollbacks) {
+ VersionedPackage firstRollback =
+ rollback.getPackages().get(0).getVersionRolledBackFrom();
+ rollbackPackage(rollback, firstRollback, rollbackReason);
+ }
+ }
+
+ /**
+ * Rollback all available low impact rollbacks
+ * @param availableRollbacks all available rollbacks
+ * @param rollbackReason reason to rollbacks
+ */
+ @WorkerThread
+ private void rollbackAllLowImpact(
+ List<RollbackInfo> availableRollbacks, @FailureReasons int rollbackReason) {
+ assertInWorkerThread();
+
+ List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
+ availableRollbacks,
+ PackageManager.ROLLBACK_USER_IMPACT_LOW);
+ if (useTwoPhaseRollback(lowImpactRollbacks)) {
+ return;
+ }
+
+ Slog.i(TAG, "Rolling back all available low impact rollbacks");
+ logCrashRecoveryEvent(Log.DEBUG, "Rolling back all available. Reason: " + rollbackReason);
+ // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all
+ // pending staged rollbacks are handled.
+ for (RollbackInfo rollback : lowImpactRollbacks) {
+ if (rollback.isStaged()) {
+ mPendingStagedRollbackIds.add(rollback.getRollbackId());
+ }
+ }
+
+ for (RollbackInfo rollback : lowImpactRollbacks) {
+ VersionedPackage firstRollback =
+ rollback.getPackages().get(0).getVersionRolledBackFrom();
+ rollbackPackage(rollback, firstRollback, rollbackReason);
+ }
+ }
+
+ private List<RollbackInfo> getRollbacksAvailableForImpactLevel(
+ List<RollbackInfo> availableRollbacks, int impactLevel) {
+ return availableRollbacks.stream()
+ .filter(rollbackInfo -> rollbackInfo.getRollbackImpactLevel() == impactLevel)
+ .toList();
+ }
+
+ private int getMinRollbackImpactLevel(List<RollbackInfo> availableRollbacks) {
+ return availableRollbacks.stream()
+ .mapToInt(RollbackInfo::getRollbackImpactLevel)
+ .min()
+ .orElse(-1);
+ }
+
+ private int getUserImpactBasedOnRollbackImpactLevel(List<RollbackInfo> availableRollbacks) {
+ int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ int minImpact = getMinRollbackImpactLevel(availableRollbacks);
+ switch (minImpact) {
+ case PackageManager.ROLLBACK_USER_IMPACT_LOW:
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
+ break;
+ case PackageManager.ROLLBACK_USER_IMPACT_HIGH:
+ if (!SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) {
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_90;
+ }
+ break;
+ default:
+ impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ return impact;
+ }
+
+ @VisibleForTesting
+ Handler getHandler() {
+ return mHandler;
+ }
+}
diff --git a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java
similarity index 100%
rename from services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
rename to packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java
new file mode 100644
index 0000000..0b7b986
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.io.File;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java
+ *
+ * @hide
+ */
+public class ArrayUtils {
+ private ArrayUtils() { /* cannot be instantiated */ }
+ public static final File[] EMPTY_FILE = new File[0];
+
+
+ /**
+ * Return first index of {@code value} in {@code array}, or {@code -1} if
+ * not found.
+ */
+ public static <T> int indexOf(@Nullable T[] array, T value) {
+ if (array == null) return -1;
+ for (int i = 0; i < array.length; i++) {
+ if (Objects.equals(array[i], value)) return i;
+ }
+ return -1;
+ }
+
+ /** @hide */
+ public static @NonNull File[] defeatNullable(@Nullable File[] val) {
+ return (val != null) ? val : EMPTY_FILE;
+ }
+
+ /**
+ * Checks if given array is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable int[] array) {
+ return array == null || array.length == 0;
+ }
+
+ /**
+ * True if the byte array is null or has length 0.
+ */
+ public static boolean isEmpty(@Nullable byte[] array) {
+ return array == null || array.length == 0;
+ }
+
+ /**
+ * Converts from List of bytes to byte array
+ * @param list
+ * @return byte[]
+ */
+ public static byte[] toPrimitive(List<byte[]> list) {
+ if (list.size() == 0) {
+ return new byte[0];
+ }
+ int byteLen = list.get(0).length;
+ byte[] array = new byte[list.size() * byteLen];
+ for (int i = 0; i < list.size(); i++) {
+ for (int j = 0; j < list.get(i).length; j++) {
+ array[i * byteLen + j] = list.get(i)[j];
+ }
+ }
+ return array;
+ }
+
+ /**
+ * Adds value to given array if not already present, providing set-like
+ * behavior.
+ */
+ public static @NonNull int[] appendInt(@Nullable int[] cur, int val) {
+ return appendInt(cur, val, false);
+ }
+
+ /**
+ * Adds value to given array.
+ */
+ public static @NonNull int[] appendInt(@Nullable int[] cur, int val,
+ boolean allowDuplicates) {
+ if (cur == null) {
+ return new int[] { val };
+ }
+ final int n = cur.length;
+ if (!allowDuplicates) {
+ for (int i = 0; i < n; i++) {
+ if (cur[i] == val) {
+ return cur;
+ }
+ }
+ }
+ int[] ret = new int[n + 1];
+ System.arraycopy(cur, 0, ret, 0, n);
+ ret[n] = val;
+ return ret;
+ }
+}
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java
new file mode 100644
index 0000000..9c73fee
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Bits and pieces copied from hidden API of android.os.FileUtils.
+ *
+ * @hide
+ */
+public class FileUtils {
+ /**
+ * Read a text file into a String, optionally limiting the length.
+ *
+ * @param file to read (will not seek, so things like /proc files are OK)
+ * @param max length (positive for head, negative of tail, 0 for no limit)
+ * @param ellipsis to add of the file was truncated (can be null)
+ * @return the contents of the file, possibly truncated
+ * @throws IOException if something goes wrong reading the file
+ * @hide
+ */
+ public static @Nullable String readTextFile(@Nullable File file, @Nullable int max,
+ @Nullable String ellipsis) throws IOException {
+ InputStream input = new FileInputStream(file);
+ // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
+ // input stream, bytes read not equal to buffer size is not necessarily the correct
+ // indication for EOF; but it is true for BufferedInputStream due to its implementation.
+ BufferedInputStream bis = new BufferedInputStream(input);
+ try {
+ long size = file.length();
+ if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes
+ if (size > 0 && (max == 0 || size < max)) max = (int) size;
+ byte[] data = new byte[max + 1];
+ int length = bis.read(data);
+ if (length <= 0) return "";
+ if (length <= max) return new String(data, 0, length);
+ if (ellipsis == null) return new String(data, 0, max);
+ return new String(data, 0, max) + ellipsis;
+ } else if (max < 0) { // "tail" mode: keep the last N
+ int len;
+ boolean rolled = false;
+ byte[] last = null;
+ byte[] data = null;
+ do {
+ if (last != null) rolled = true;
+ byte[] tmp = last;
+ last = data;
+ data = tmp;
+ if (data == null) data = new byte[-max];
+ len = bis.read(data);
+ } while (len == data.length);
+
+ if (last == null && len <= 0) return "";
+ if (last == null) return new String(data, 0, len);
+ if (len > 0) {
+ rolled = true;
+ System.arraycopy(last, len, last, 0, last.length - len);
+ System.arraycopy(data, 0, last, last.length - len, len);
+ }
+ if (ellipsis == null || !rolled) return new String(last);
+ return ellipsis + new String(last);
+ } else { // "cat" mode: size unknown, read it all in streaming fashion
+ ByteArrayOutputStream contents = new ByteArrayOutputStream();
+ int len;
+ byte[] data = new byte[1024];
+ do {
+ len = bis.read(data);
+ if (len > 0) contents.write(data, 0, len);
+ } while (len == data.length);
+ return contents.toString();
+ }
+ } finally {
+ bis.close();
+ input.close();
+ }
+ }
+
+ /**
+ * Perform an fsync on the given FileOutputStream. The stream at this
+ * point must be flushed but not yet closed.
+ *
+ * @hide
+ */
+ public static boolean sync(FileOutputStream stream) {
+ try {
+ if (stream != null) {
+ stream.getFD().sync();
+ }
+ return true;
+ } catch (IOException e) {
+ }
+ return false;
+ }
+
+ /**
+ * List the files in the directory or return empty file.
+ *
+ * @hide
+ */
+ public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
+ return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles())
+ : ArrayUtils.EMPTY_FILE;
+ }
+}
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java
new file mode 100644
index 0000000..9a24ada
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import libcore.util.EmptyArray;
+
+import java.util.NoSuchElementException;
+
+/**
+ * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java
+ *
+ * @hide
+ */
+public class LongArrayQueue {
+
+ private long[] mValues;
+ private int mSize;
+ private int mHead;
+ private int mTail;
+
+ private long[] newUnpaddedLongArray(int num) {
+ return new long[num];
+ }
+ /**
+ * Initializes a queue with the given starting capacity.
+ *
+ * @param initialCapacity the capacity.
+ */
+ public LongArrayQueue(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mValues = EmptyArray.LONG;
+ } else {
+ mValues = newUnpaddedLongArray(initialCapacity);
+ }
+ mSize = 0;
+ mHead = mTail = 0;
+ }
+
+ /**
+ * Initializes a queue with default starting capacity.
+ */
+ public LongArrayQueue() {
+ this(16);
+ }
+
+ /** @hide */
+ public static int growSize(int currentSize) {
+ return currentSize <= 4 ? 8 : currentSize * 2;
+ }
+
+ private void grow() {
+ if (mSize < mValues.length) {
+ throw new IllegalStateException("Queue not full yet!");
+ }
+ final int newSize = growSize(mSize);
+ final long[] newArray = newUnpaddedLongArray(newSize);
+ final int r = mValues.length - mHead; // Number of elements on and to the right of head.
+ System.arraycopy(mValues, mHead, newArray, 0, r);
+ System.arraycopy(mValues, 0, newArray, r, mHead);
+ mValues = newArray;
+ mHead = 0;
+ mTail = mSize;
+ }
+
+ /**
+ * Returns the number of elements in the queue.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Removes all elements from this queue.
+ */
+ public void clear() {
+ mSize = 0;
+ mHead = mTail = 0;
+ }
+
+ /**
+ * Adds a value to the tail of the queue.
+ *
+ * @param value the value to be added.
+ */
+ public void addLast(long value) {
+ if (mSize == mValues.length) {
+ grow();
+ }
+ mValues[mTail] = value;
+ mTail = (mTail + 1) % mValues.length;
+ mSize++;
+ }
+
+ /**
+ * Removes an element from the head of the queue.
+ *
+ * @return the element at the head of the queue.
+ * @throws NoSuchElementException if the queue is empty.
+ */
+ public long removeFirst() {
+ if (mSize == 0) {
+ throw new NoSuchElementException("Queue is empty!");
+ }
+ final long ret = mValues[mHead];
+ mHead = (mHead + 1) % mValues.length;
+ mSize--;
+ return ret;
+ }
+
+ /**
+ * Returns the element at the given position from the head of the queue, where 0 represents the
+ * head of the queue.
+ *
+ * @param position the position from the head of the queue.
+ * @return the element found at the given position.
+ * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or
+ * {@code position} >= {@link #size()}
+ */
+ public long get(int position) {
+ if (position < 0 || position >= mSize) {
+ throw new IndexOutOfBoundsException("Index " + position
+ + " not valid for a queue of size " + mSize);
+ }
+ final int index = (mHead + position) % mValues.length;
+ return mValues[index];
+ }
+
+ /**
+ * Returns the element at the head of the queue, without removing it.
+ *
+ * @return the element at the head of the queue.
+ * @throws NoSuchElementException if the queue is empty
+ */
+ public long peekFirst() {
+ if (mSize == 0) {
+ throw new NoSuchElementException("Queue is empty!");
+ }
+ return mValues[mHead];
+ }
+
+ /**
+ * Returns the element at the tail of the queue.
+ *
+ * @return the element at the tail of the queue.
+ * @throws NoSuchElementException if the queue is empty.
+ */
+ public long peekLast() {
+ if (mSize == 0) {
+ throw new NoSuchElementException("Queue is empty!");
+ }
+ final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1;
+ return mValues[index];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ if (mSize <= 0) {
+ return "{}";
+ }
+
+ final StringBuilder buffer = new StringBuilder(mSize * 64);
+ buffer.append('{');
+ buffer.append(get(0));
+ for (int i = 1; i < mSize; i++) {
+ buffer.append(", ");
+ buffer.append(get(i));
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java
new file mode 100644
index 0000000..50823f5
--- /dev/null
+++ b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.modules.utils.TypedXmlPullParser;
+
+import libcore.util.XmlObjectFactory;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Bits and pieces copied from hidden API of
+ * frameworks/base/core/java/com/android/internal/util/XmlUtils.java
+ *
+ * @hide
+ */
+public class XmlUtils {
+
+ private static final String STRING_ARRAY_SEPARATOR = ":";
+
+ /** @hide */
+ public static final void beginDocument(XmlPullParser parser, String firstElementName)
+ throws XmlPullParserException, IOException {
+ int type;
+ while ((type = parser.next()) != parser.START_TAG
+ && type != parser.END_DOCUMENT) {
+ // Do nothing
+ }
+
+ if (type != parser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+
+ if (!parser.getName().equals(firstElementName)) {
+ throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
+ + ", expected " + firstElementName);
+ }
+ }
+
+ /** @hide */
+ public static boolean nextElementWithin(XmlPullParser parser, int outerDepth)
+ throws IOException, XmlPullParserException {
+ for (;;) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_DOCUMENT
+ || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) {
+ return false;
+ }
+ if (type == XmlPullParser.START_TAG
+ && parser.getDepth() == outerDepth + 1) {
+ return true;
+ }
+ }
+ }
+
+ private static XmlPullParser newPullParser() {
+ try {
+ XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ return parser;
+ } catch (XmlPullParserException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /** @hide */
+ public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
+ throws IOException {
+ final byte[] magic = new byte[4];
+ if (in instanceof FileInputStream) {
+ try {
+ Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ } else {
+ if (!in.markSupported()) {
+ in = new BufferedInputStream(in);
+ }
+ in.mark(8);
+ in.read(magic);
+ in.reset();
+ }
+
+ final TypedXmlPullParser xml;
+ xml = (TypedXmlPullParser) newPullParser();
+ try {
+ xml.setInput(in, "UTF_8");
+ } catch (XmlPullParserException e) {
+ throw new IOException(e);
+ }
+ return xml;
+ }
+}
diff --git a/services/core/java/com/android/server/ExplicitHealthCheckController.java b/packages/CrashRecovery/services/platform/java/com/android/server/ExplicitHealthCheckController.java
similarity index 100%
rename from services/core/java/com/android/server/ExplicitHealthCheckController.java
rename to packages/CrashRecovery/services/platform/java/com/android/server/ExplicitHealthCheckController.java
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/platform/java/com/android/server/PackageWatchdog.java
similarity index 100%
rename from services/core/java/com/android/server/PackageWatchdog.java
rename to packages/CrashRecovery/services/platform/java/com/android/server/PackageWatchdog.java
diff --git a/services/core/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/platform/java/com/android/server/RescueParty.java
similarity index 100%
rename from services/core/java/com/android/server/RescueParty.java
rename to packages/CrashRecovery/services/platform/java/com/android/server/RescueParty.java
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/packages/CrashRecovery/services/platform/java/com/android/server/crashrecovery/CrashRecoveryModule.java
similarity index 100%
copy from services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java
copy to packages/CrashRecovery/services/platform/java/com/android/server/crashrecovery/CrashRecoveryModule.java
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/packages/CrashRecovery/services/platform/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
similarity index 100%
copy from services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
copy to packages/CrashRecovery/services/platform/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/platform/java/com/android/server/rollback/RollbackPackageHealthObserver.java
similarity index 100%
rename from services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
rename to packages/CrashRecovery/services/platform/java/com/android/server/rollback/RollbackPackageHealthObserver.java
diff --git a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/platform/java/com/android/server/rollback/WatchdogRollbackLogger.java
similarity index 100%
copy from services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java
copy to packages/CrashRecovery/services/platform/java/com/android/server/rollback/WatchdogRollbackLogger.java
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index c4a45d0..2863531 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -802,6 +802,7 @@
"SystemUICustomizationTestUtils",
"androidx.compose.runtime_runtime",
"kosmos",
+ "testables",
"androidx.test.rules",
],
libs: [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index d025275..93a99bd 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -136,6 +136,23 @@
)
/**
+ * The timings when animating a View into an app using a spring animator.
+ *
+ * Important: since springs don't have fixed durations, these timings represent fractions of
+ * the progress between the spring's initial value and its final value.
+ *
+ * TODO(b/372858592): make this a separate class explicitly using percentages.
+ */
+ val SPRING_TIMINGS =
+ TransitionAnimator.Timings(
+ totalDuration = 1000L,
+ contentBeforeFadeOutDelay = 0L,
+ contentBeforeFadeOutDuration = 800L,
+ contentAfterFadeInDelay = 850L,
+ contentAfterFadeInDuration = 135L,
+ )
+
+ /**
* The timings when animating a Dialog into an app. We need to wait at least 200ms before
* showing the app (which is under the dialog window) so that the dialog window dim is fully
* faded out, to avoid flicker.
@@ -152,6 +169,13 @@
contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f),
)
+ /** The interpolators when animating a View into an app using a spring animator. */
+ val SPRING_INTERPOLATORS =
+ INTERPOLATORS.copy(
+ contentBeforeFadeOutInterpolator = Interpolators.DECELERATE_1_5,
+ contentAfterFadeInInterpolator = Interpolators.SLOW_OUT_LINEAR_IN,
+ )
+
// TODO(b/288507023): Remove this flag.
@JvmField val DEBUG_TRANSITION_ANIMATION = Build.IS_DEBUGGABLE
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 3dc0657..1d8ff77 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -23,6 +23,7 @@
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.drawable.GradientDrawable
+import android.util.FloatProperty
import android.util.Log
import android.util.MathUtils
import android.view.View
@@ -31,10 +32,15 @@
import android.view.ViewOverlay
import android.view.animation.Interpolator
import android.window.WindowAnimationState
-import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators.LINEAR
+import com.android.app.animation.MathUtils.max
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.dynamicanimation.animation.SpringAnimation
+import com.android.internal.dynamicanimation.animation.SpringForce
import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
import java.util.concurrent.Executor
+import kotlin.math.abs
+import kotlin.math.min
import kotlin.math.roundToInt
private const val TAG = "TransitionAnimator"
@@ -44,11 +50,27 @@
private val mainExecutor: Executor,
private val timings: Timings,
private val interpolators: Interpolators,
+
+ /** [springTimings] and [springInterpolators] must either both be null or both not null. */
+ private val springTimings: Timings? = null,
+ private val springInterpolators: Interpolators? = null,
+ private val springParams: SpringParams = DEFAULT_SPRING_PARAMS,
) {
companion object {
internal const val DEBUG = false
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
+ /** Default parameters for the multi-spring animator. */
+ private val DEFAULT_SPRING_PARAMS =
+ SpringParams(
+ centerXStiffness = 450f,
+ centerXDampingRatio = 0.965f,
+ centerYStiffness = 400f,
+ centerYDampingRatio = 0.95f,
+ scaleStiffness = 500f,
+ scaleDampingRatio = 0.99f,
+ )
+
/**
* Given the [linearProgress] of a transition animation, return the linear progress of the
* sub-animation starting [delay] ms after the transition animation and that lasts
@@ -86,11 +108,32 @@
it.bottomCornerRadius = (bottomLeftRadius + bottomRightRadius) / 2
it.topCornerRadius = (topLeftRadius + topRightRadius) / 2
}
+
+ /** Builds a [FloatProperty] for updating the defined [property] using a spring. */
+ private fun buildProperty(
+ property: SpringProperty,
+ updateProgress: (SpringState) -> Unit,
+ ): FloatProperty<SpringState> {
+ return object : FloatProperty<SpringState>(property.name) {
+ override fun get(state: SpringState): Float {
+ return property.get(state)
+ }
+
+ override fun setValue(state: SpringState, value: Float) {
+ property.setValue(state, value)
+ updateProgress(state)
+ }
+ }
+ }
}
private val transitionContainerLocation = IntArray(2)
private val cornerRadii = FloatArray(8)
+ init {
+ check((springTimings == null) == (springInterpolators == null))
+ }
+
/**
* A controller that takes care of applying the animation to an expanding view.
*
@@ -198,6 +241,65 @@
var visible: Boolean = true
}
+ /** Encapsulated the state of a multi-spring animation. */
+ internal class SpringState(
+ // Animated values.
+ var centerX: Float,
+ var centerY: Float,
+ var scale: Float = 0f,
+
+ // Cached values.
+ var previousCenterX: Float = -1f,
+ var previousCenterY: Float = -1f,
+ var previousScale: Float = -1f,
+
+ // Completion flags.
+ var isCenterXDone: Boolean = false,
+ var isCenterYDone: Boolean = false,
+ var isScaleDone: Boolean = false,
+ ) {
+ /** Whether all springs composing the animation have settled in the final position. */
+ val isDone
+ get() = isCenterXDone && isCenterYDone && isScaleDone
+ }
+
+ /** Supported [SpringState] properties with getters and setters to update them. */
+ private enum class SpringProperty {
+ CENTER_X {
+ override fun get(state: SpringState): Float {
+ return state.centerX
+ }
+
+ override fun setValue(state: SpringState, value: Float) {
+ state.centerX = value
+ }
+ },
+ CENTER_Y {
+ override fun get(state: SpringState): Float {
+ return state.centerY
+ }
+
+ override fun setValue(state: SpringState, value: Float) {
+ state.centerY = value
+ }
+ },
+ SCALE {
+ override fun get(state: SpringState): Float {
+ return state.scale
+ }
+
+ override fun setValue(state: SpringState, value: Float) {
+ state.scale = value
+ }
+ };
+
+ /** Extracts the current value of the underlying property from [state]. */
+ abstract fun get(state: SpringState): Float
+
+ /** Update's the [value] of the underlying property inside [state]. */
+ abstract fun setValue(state: SpringState, value: Float)
+ }
+
interface Animation {
/** Start the animation. */
fun start()
@@ -217,6 +319,33 @@
}
}
+ @VisibleForTesting
+ class MultiSpringAnimation
+ internal constructor(
+ @get:VisibleForTesting val springX: SpringAnimation,
+ @get:VisibleForTesting val springY: SpringAnimation,
+ @get:VisibleForTesting val springScale: SpringAnimation,
+ private val springState: SpringState,
+ private val onAnimationStart: Runnable,
+ ) : Animation {
+ @get:VisibleForTesting
+ val isDone
+ get() = springState.isDone
+
+ override fun start() {
+ onAnimationStart.run()
+ springX.start()
+ springY.start()
+ springScale.start()
+ }
+
+ override fun cancel() {
+ springX.cancel()
+ springY.cancel()
+ springScale.cancel()
+ }
+ }
+
/** The timings (durations and delays) used by this animator. */
data class Timings(
/** The total duration of the animation. */
@@ -256,6 +385,21 @@
val contentAfterFadeInInterpolator: Interpolator,
)
+ /** The parameters (stiffnesses and damping ratios) used by the multi-spring animator. */
+ data class SpringParams(
+ // Parameters for the X position spring.
+ val centerXStiffness: Float,
+ val centerXDampingRatio: Float,
+
+ // Parameters for the Y position spring.
+ val centerYStiffness: Float,
+ val centerYDampingRatio: Float,
+
+ // Parameters for the scale spring.
+ val scaleStiffness: Float,
+ val scaleDampingRatio: Float,
+ )
+
/**
* Start a transition animation controlled by [controller] towards [endState]. An intermediary
* layer with [windowBackgroundColor] will fade in then (optionally) fade out above the
@@ -266,6 +410,9 @@
* the animation (if ![Controller.isLaunching]), and will have SRC blending mode (ultimately
* punching a hole in the [transition container][Controller.transitionContainer]) iff [drawHole]
* is true.
+ *
+ * If [useSpring] is true, a multi-spring animation will be used instead of the default
+ * interpolators.
*/
fun startAnimation(
controller: Controller,
@@ -273,8 +420,9 @@
windowBackgroundColor: Int,
fadeWindowBackgroundLayer: Boolean = true,
drawHole: Boolean = false,
+ useSpring: Boolean = false,
): Animation {
- if (!controller.isLaunching) checkReturnAnimationFrameworkFlag()
+ if (!controller.isLaunching || useSpring) checkReturnAnimationFrameworkFlag()
// We add an extra layer with the same color as the dialog/app splash screen background
// color, which is usually the same color of the app background. We first fade in this layer
@@ -293,6 +441,7 @@
windowBackgroundLayer,
fadeWindowBackgroundLayer,
drawHole,
+ useSpring,
)
.apply { start() }
}
@@ -304,6 +453,7 @@
endState: State,
windowBackgroundLayer: GradientDrawable,
fadeWindowBackgroundLayer: Boolean = true,
+ useSpring: Boolean = false,
drawHole: Boolean = false,
): Animation {
val transitionContainer = controller.transitionContainer
@@ -321,19 +471,35 @@
openingWindowSyncView != null &&
openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
- return createInterpolatedAnimation(
- controller,
- startState,
- endState,
- windowBackgroundLayer,
- transitionContainer,
- transitionContainerOverlay,
- openingWindowSyncView,
- openingWindowSyncViewOverlay,
- fadeWindowBackgroundLayer,
- drawHole,
- moveBackgroundLayerWhenAppVisibilityChanges,
- )
+ return if (useSpring && springTimings != null && springInterpolators != null) {
+ createSpringAnimation(
+ controller,
+ startState,
+ endState,
+ windowBackgroundLayer,
+ transitionContainer,
+ transitionContainerOverlay,
+ openingWindowSyncView,
+ openingWindowSyncViewOverlay,
+ fadeWindowBackgroundLayer,
+ drawHole,
+ moveBackgroundLayerWhenAppVisibilityChanges,
+ )
+ } else {
+ createInterpolatedAnimation(
+ controller,
+ startState,
+ endState,
+ windowBackgroundLayer,
+ transitionContainer,
+ transitionContainerOverlay,
+ openingWindowSyncView,
+ openingWindowSyncViewOverlay,
+ fadeWindowBackgroundLayer,
+ drawHole,
+ moveBackgroundLayerWhenAppVisibilityChanges,
+ )
+ }
}
/**
@@ -478,6 +644,7 @@
fadeWindowBackgroundLayer,
drawHole,
controller.isLaunching,
+ useSpring = false,
)
controller.onTransitionAnimationProgress(state, progress, linearProgress)
@@ -486,6 +653,215 @@
return InterpolatedAnimation(animator)
}
+ /**
+ * Creates a compound animator made up of three springs: one for the center x position, one for
+ * the center-y position, and one for the overall scale.
+ *
+ * This animator uses [springTimings] and [springInterpolators] for opacity, based on the scale
+ * progress.
+ */
+ private fun createSpringAnimation(
+ controller: Controller,
+ startState: State,
+ endState: State,
+ windowBackgroundLayer: GradientDrawable,
+ transitionContainer: View,
+ transitionContainerOverlay: ViewGroupOverlay,
+ openingWindowSyncView: View?,
+ openingWindowSyncViewOverlay: ViewOverlay?,
+ fadeWindowBackgroundLayer: Boolean = true,
+ drawHole: Boolean = false,
+ moveBackgroundLayerWhenAppVisibilityChanges: Boolean = false,
+ ): Animation {
+ var springX: SpringAnimation? = null
+ var springY: SpringAnimation? = null
+ var targetX = endState.centerX
+ var targetY = endState.centerY
+
+ var movedBackgroundLayer = false
+
+ fun maybeUpdateEndState() {
+ if (endState.centerX != targetX && endState.centerY != targetY) {
+ targetX = endState.centerX
+ targetY = endState.centerY
+
+ springX?.animateToFinalPosition(targetX)
+ springY?.animateToFinalPosition(targetY)
+ }
+ }
+
+ fun updateProgress(state: SpringState) {
+ if (
+ (!state.isCenterXDone && state.centerX == state.previousCenterX) ||
+ (!state.isCenterYDone && state.centerY == state.previousCenterY) ||
+ (!state.isScaleDone && state.scale == state.previousScale)
+ ) {
+ // Because all three springs use the same update method, we only actually update
+ // when all values have changed, avoiding two redundant calls per frame.
+ return
+ }
+
+ // Update the latest values for the check above.
+ state.previousCenterX = state.centerX
+ state.previousCenterY = state.centerY
+ state.previousScale = state.scale
+
+ // Current scale-based values, that will be used to find the new animation bounds.
+ val width =
+ MathUtils.lerp(startState.width.toFloat(), endState.width.toFloat(), state.scale)
+ val height =
+ MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(), state.scale)
+
+ val newState =
+ State(
+ left = (state.centerX - width / 2).toInt(),
+ top = (state.centerY - height / 2).toInt(),
+ right = (state.centerX + width / 2).toInt(),
+ bottom = (state.centerY + height / 2).toInt(),
+ topCornerRadius =
+ MathUtils.lerp(
+ startState.topCornerRadius,
+ endState.topCornerRadius,
+ state.scale,
+ ),
+ bottomCornerRadius =
+ MathUtils.lerp(
+ startState.bottomCornerRadius,
+ endState.bottomCornerRadius,
+ state.scale,
+ ),
+ )
+ .apply {
+ visible = checkVisibility(timings, state.scale, controller.isLaunching)
+ }
+
+ if (!movedBackgroundLayer) {
+ movedBackgroundLayer =
+ maybeMoveBackgroundLayer(
+ controller,
+ newState,
+ windowBackgroundLayer,
+ transitionContainer,
+ transitionContainerOverlay,
+ openingWindowSyncView,
+ openingWindowSyncViewOverlay,
+ moveBackgroundLayerWhenAppVisibilityChanges,
+ )
+ }
+
+ val container =
+ if (movedBackgroundLayer) {
+ openingWindowSyncView!!
+ } else {
+ controller.transitionContainer
+ }
+ applyStateToWindowBackgroundLayer(
+ windowBackgroundLayer,
+ newState,
+ state.scale,
+ container,
+ fadeWindowBackgroundLayer,
+ drawHole,
+ isLaunching = false,
+ useSpring = true,
+ )
+
+ controller.onTransitionAnimationProgress(newState, state.scale, state.scale)
+
+ maybeUpdateEndState()
+ }
+
+ val springState = SpringState(centerX = startState.centerX, centerY = startState.centerY)
+ val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
+
+ /** End listener for each spring, which only does the end work if all springs are done. */
+ fun onAnimationEnd() {
+ if (!springState.isDone) return
+ onAnimationEnd(
+ controller,
+ isExpandingFullyAbove,
+ windowBackgroundLayer,
+ transitionContainerOverlay,
+ openingWindowSyncViewOverlay,
+ moveBackgroundLayerWhenAppVisibilityChanges,
+ )
+ }
+
+ springX =
+ SpringAnimation(
+ springState,
+ buildProperty(SpringProperty.CENTER_X) { state -> updateProgress(state) },
+ )
+ .apply {
+ spring =
+ SpringForce(endState.centerX).apply {
+ stiffness = springParams.centerXStiffness
+ dampingRatio = springParams.centerXDampingRatio
+ }
+
+ setStartValue(startState.centerX)
+ setMinValue(min(startState.centerX, endState.centerX))
+ setMaxValue(max(startState.centerX, endState.centerX))
+
+ addEndListener { _, _, _, _ ->
+ springState.isCenterXDone = true
+ onAnimationEnd()
+ }
+ }
+ springY =
+ SpringAnimation(
+ springState,
+ buildProperty(SpringProperty.CENTER_Y) { state -> updateProgress(state) },
+ )
+ .apply {
+ spring =
+ SpringForce(endState.centerY).apply {
+ stiffness = springParams.centerYStiffness
+ dampingRatio = springParams.centerYDampingRatio
+ }
+
+ setStartValue(startState.centerY)
+ setMinValue(min(startState.centerY, endState.centerY))
+ setMaxValue(max(startState.centerY, endState.centerY))
+
+ addEndListener { _, _, _, _ ->
+ springState.isCenterYDone = true
+ onAnimationEnd()
+ }
+ }
+ val springScale =
+ SpringAnimation(
+ springState,
+ buildProperty(SpringProperty.SCALE) { state -> updateProgress(state) },
+ )
+ .apply {
+ spring =
+ SpringForce(1f).apply {
+ stiffness = springParams.scaleStiffness
+ dampingRatio = springParams.scaleDampingRatio
+ }
+
+ setStartValue(0f)
+ setMaxValue(1f)
+ setMinimumVisibleChange(abs(1f / startState.height))
+
+ addEndListener { _, _, _, _ ->
+ springState.isScaleDone = true
+ onAnimationEnd()
+ }
+ }
+
+ return MultiSpringAnimation(springX, springY, springScale, springState) {
+ onAnimationStart(
+ controller,
+ isExpandingFullyAbove,
+ windowBackgroundLayer,
+ transitionContainerOverlay,
+ openingWindowSyncViewOverlay,
+ )
+ }
+ }
+
private fun onAnimationStart(
controller: Controller,
isExpandingFullyAbove: Boolean,
@@ -623,6 +999,7 @@
fadeWindowBackgroundLayer: Boolean,
drawHole: Boolean,
isLaunching: Boolean,
+ useSpring: Boolean,
) {
// Update position.
transitionContainer.getLocationOnScreen(transitionContainerLocation)
@@ -644,8 +1021,19 @@
cornerRadii[7] = state.bottomCornerRadius
drawable.cornerRadii = cornerRadii
- // We first fade in the background layer to hide the expanding view, then fade it out
- // with SRC mode to draw a hole punch in the status bar and reveal the opening window.
+ val timings: Timings
+ val interpolators: Interpolators
+ if (useSpring) {
+ timings = springTimings!!
+ interpolators = springInterpolators!!
+ } else {
+ timings = this.timings
+ interpolators = this.interpolators
+ }
+
+ // We first fade in the background layer to hide the expanding view, then fade it out with
+ // SRC mode to draw a hole punch in the status bar and reveal the opening window (if
+ // needed). If !isLaunching, the reverse happens.
val fadeInProgress =
getProgress(
timings,
@@ -653,6 +1041,13 @@
timings.contentBeforeFadeOutDelay,
timings.contentBeforeFadeOutDuration,
)
+ val fadeOutProgress =
+ getProgress(
+ timings,
+ linearProgress,
+ timings.contentAfterFadeInDelay,
+ timings.contentAfterFadeInDuration,
+ )
if (isLaunching) {
if (fadeInProgress < 1) {
@@ -660,13 +1055,6 @@
interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
drawable.alpha = (alpha * 0xFF).roundToInt()
} else if (fadeWindowBackgroundLayer) {
- val fadeOutProgress =
- getProgress(
- timings,
- linearProgress,
- timings.contentAfterFadeInDelay,
- timings.contentAfterFadeInDuration,
- )
val alpha =
1 -
interpolators.contentAfterFadeInInterpolator.getInterpolation(
@@ -690,13 +1078,6 @@
drawable.setXfermode(SRC_MODE)
}
} else {
- val fadeOutProgress =
- getProgress(
- timings,
- linearProgress,
- timings.contentAfterFadeInDelay,
- timings.contentAfterFadeInDuration,
- )
val alpha =
1 -
interpolators.contentAfterFadeInInterpolator.getInterpolation(
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
index 4fe9f89..dc6e0ce 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation
+import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.core.animation.Interpolator
import com.android.app.animation.InterpolatorsAndroidX
@@ -59,6 +60,17 @@
/** The linear interpolator. */
val Linear = fromInterpolator(InterpolatorsAndroidX.LINEAR)
+ /**
+ * Use this easing for animating progress values coming from the back callback to get the
+ * predictive-back-typical decelerate motion.
+ *
+ * This easing is similar to [StandardDecelerate] but has a slight acceleration phase at the
+ * start.
+ *
+ * See also [InterpolatorsAndroidX.BACK_GESTURE].
+ */
+ val PredictiveBack = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+
/** The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN. */
val Legacy = fromInterpolator(InterpolatorsAndroidX.LEGACY)
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
index 6b3223d..1256641 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
@@ -18,6 +18,7 @@
import android.content.Context
import androidx.annotation.ColorRes
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import com.android.internal.R
@@ -38,23 +39,41 @@
* [androidx.compose.material3.MaterialTheme]. For other colors (e.g. primary), use
* `MaterialTheme.colorScheme` instead.
*/
-class AndroidColorScheme(val context: Context) {
- val primaryFixed = color(context, R.color.system_primary_fixed)
- val primaryFixedDim = color(context, R.color.system_primary_fixed_dim)
- val onPrimaryFixed = color(context, R.color.system_on_primary_fixed)
- val onPrimaryFixedVariant = color(context, R.color.system_on_primary_fixed_variant)
- val secondaryFixed = color(context, R.color.system_secondary_fixed)
- val secondaryFixedDim = color(context, R.color.system_secondary_fixed_dim)
- val onSecondaryFixed = color(context, R.color.system_on_secondary_fixed)
- val onSecondaryFixedVariant = color(context, R.color.system_on_secondary_fixed_variant)
- val tertiaryFixed = color(context, R.color.system_tertiary_fixed)
- val tertiaryFixedDim = color(context, R.color.system_tertiary_fixed_dim)
- val onTertiaryFixed = color(context, R.color.system_on_tertiary_fixed)
- val onTertiaryFixedVariant = color(context, R.color.system_on_tertiary_fixed_variant)
-
+@Immutable
+class AndroidColorScheme(
+ val primaryFixed: Color,
+ val primaryFixedDim: Color,
+ val onPrimaryFixed: Color,
+ val onPrimaryFixedVariant: Color,
+ val secondaryFixed: Color,
+ val secondaryFixedDim: Color,
+ val onSecondaryFixed: Color,
+ val onSecondaryFixedVariant: Color,
+ val tertiaryFixed: Color,
+ val tertiaryFixedDim: Color,
+ val onTertiaryFixed: Color,
+ val onTertiaryFixedVariant: Color,
+) {
companion object {
internal fun color(context: Context, @ColorRes id: Int): Color {
return Color(context.resources.getColor(id, context.theme))
}
+
+ operator fun invoke(context: Context): AndroidColorScheme {
+ return AndroidColorScheme(
+ primaryFixed = color(context, R.color.system_primary_fixed),
+ primaryFixedDim = color(context, R.color.system_primary_fixed_dim),
+ onPrimaryFixed = color(context, R.color.system_on_primary_fixed),
+ onPrimaryFixedVariant = color(context, R.color.system_on_primary_fixed_variant),
+ secondaryFixed = color(context, R.color.system_secondary_fixed),
+ secondaryFixedDim = color(context, R.color.system_secondary_fixed_dim),
+ onSecondaryFixed = color(context, R.color.system_on_secondary_fixed),
+ onSecondaryFixedVariant = color(context, R.color.system_on_secondary_fixed_variant),
+ tertiaryFixed = color(context, R.color.system_tertiary_fixed),
+ tertiaryFixedDim = color(context, R.color.system_tertiary_fixed_dim),
+ onTertiaryFixed = color(context, R.color.system_on_tertiary_fixed),
+ onTertiaryFixedVariant = color(context, R.color.system_on_tertiary_fixed_variant),
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
index dd37b53..cdf8d00 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
@@ -1,6 +1,6 @@
package com.android.systemui.scene.ui.composable.transitions
-import androidx.compose.animation.core.CubicBezierEasing
+import com.android.compose.animation.Easings
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.bouncer.ui.composable.Bouncer
@@ -9,7 +9,7 @@
}
fun TransitionBuilder.bouncerToLockscreenPreview() {
- fractionRange(easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)) {
+ fractionRange(easing = Easings.PredictiveBack) {
scaleDraw(Bouncer.Elements.Content, scaleY = 0.8f, scaleX = 0.8f)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepositoryTest.kt
new file mode 100644
index 0000000..fd1f52b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepositoryTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class QSColumnsRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private lateinit var underTest: QSColumnsRepository
+
+ @Before
+ fun setUp() {
+ underTest = with(kosmos) { qsColumnsRepository }
+ }
+
+ @Test
+ fun configChanges_triggerColumnsUpdate() =
+ with(kosmos) {
+ testScope.runTest {
+ val latest by collectLastValue(underTest.columns)
+
+ setColumnsInConfig(4)
+ assertThat(latest).isEqualTo(4)
+
+ setColumnsInConfig(8)
+ assertThat(latest).isEqualTo(8)
+ }
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun withDualShade_returnsCorrectValue() =
+ with(kosmos) {
+ testScope.runTest {
+ val latest by collectLastValue(underTest.columns)
+ assertThat(latest).isEqualTo(4)
+
+ setColumnsInConfig(8, id = R.integer.quick_settings_dual_shade_num_columns)
+ // Asserts config changes are ignored
+ assertThat(latest).isEqualTo(4)
+ }
+ }
+
+ private fun setColumnsInConfig(
+ columns: Int,
+ id: Int = R.integer.quick_settings_infinite_grid_num_columns,
+ ) =
+ with(kosmos) {
+ testCase.context.orCreateTestableResources.addOverride(id, columns)
+ fakeConfigurationRepository.onConfigurationChange()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
index ef85302..a1c0ef2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
@@ -58,6 +58,11 @@
qsPreferencesInteractor.setLargeTilesSpecs(
tiles.filter { it.spec.startsWith(PREFIX_LARGE) }.toSet()
)
+ testCase.context.orCreateTestableResources.addOverride(
+ R.integer.quick_settings_infinite_grid_num_columns,
+ 4,
+ )
+ fakeConfigurationRepository.onConfigurationChange()
}
private val underTest = kosmos.quickQuickSettingsViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
similarity index 90%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
index cd18925..40c3f22 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
@@ -31,15 +31,15 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class BackGestureMonitorTest : SysuiTestCase() {
+class BackGestureRecognizerTest : SysuiTestCase() {
private var gestureState: GestureState = NotStarted
- private val gestureMonitor =
- BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+ private val gestureRecognizer =
+ BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
@Before
fun before() {
- gestureMonitor.addGestureStateCallback { gestureState = it }
+ gestureRecognizer.addGestureStateCallback { gestureState = it }
}
@Test
@@ -85,7 +85,7 @@
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.accept(it) }
+ events.forEach { gestureRecognizer.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
index 3f1633a..8406d3b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
@@ -36,7 +36,7 @@
private var triggered = false
private val handler =
TouchpadGestureHandler(
- BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()),
+ BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()),
EasterEggGestureMonitor(callback = { triggered = true }),
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
similarity index 90%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
index edf0e56..043b775 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
@@ -31,15 +31,15 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class HomeGestureMonitorTest : SysuiTestCase() {
+class HomeGestureRecognizerTest : SysuiTestCase() {
private var gestureState: GestureState = GestureState.NotStarted
- private val gestureMonitor =
- HomeGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+ private val gestureRecognizer =
+ HomeGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
@Before
fun before() {
- gestureMonitor.addGestureStateCallback { gestureState = it }
+ gestureRecognizer.addGestureStateCallback { gestureState = it }
}
@Test
@@ -81,7 +81,7 @@
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.accept(it) }
+ events.forEach { gestureRecognizer.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
index f68e773..7095a91 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
@@ -35,7 +35,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class RecentAppsGestureMonitorTest : SysuiTestCase() {
+class RecentAppsGestureRecognizerTest : SysuiTestCase() {
companion object {
const val THRESHOLD_VELOCITY_PX_PER_MS = 0.1f
@@ -50,8 +50,8 @@
// by default return correct speed for the gesture - as if pointer is slowing down
on { calculateVelocity() } doReturn SLOW
}
- private val gestureMonitor =
- RecentAppsGestureMonitor(
+ private val gestureRecognizer =
+ RecentAppsGestureRecognizer(
gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
velocityThresholdPxPerMs = THRESHOLD_VELOCITY_PX_PER_MS,
velocityTracker = VerticalVelocityTracker(velocityTracker1D),
@@ -59,7 +59,7 @@
@Before
fun before() {
- gestureMonitor.addGestureStateCallback { gestureState = it }
+ gestureRecognizer.addGestureStateCallback { gestureState = it }
}
@Test
@@ -107,7 +107,7 @@
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.accept(it) }
+ events.forEach { gestureRecognizer.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 9f7ea679..a867eb3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -36,13 +36,13 @@
class TouchpadGestureHandlerTest : SysuiTestCase() {
private var gestureState: GestureState = GestureState.NotStarted
- private val gestureMonitor =
- BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
- private val handler = TouchpadGestureHandler(gestureMonitor, EasterEggGestureMonitor {})
+ private val gestureRecognizer =
+ BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+ private val handler = TouchpadGestureHandler(gestureRecognizer, EasterEggGestureMonitor {})
@Before
fun before() {
- gestureMonitor.addGestureStateCallback { gestureState = it }
+ gestureRecognizer.addGestureStateCallback { gestureState = it }
}
@Test
diff --git a/packages/SystemUI/res/values-land/config.xml b/packages/SystemUI/res/values-land/config.xml
index db526b1..b5efeb5 100644
--- a/packages/SystemUI/res/values-land/config.xml
+++ b/packages/SystemUI/res/values-land/config.xml
@@ -25,6 +25,9 @@
<integer name="quick_settings_num_columns">4</integer>
+ <!-- The number of columns in the infinite grid QuickSettings -->
+ <integer name="quick_settings_infinite_grid_num_columns">8</integer>
+
<!-- The number of columns that the top level tiles span in the QuickSettings -->
<integer name="quick_settings_user_time_settings_tile_span">2</integer>
diff --git a/packages/SystemUI/res/values-sw600dp-port/config.xml b/packages/SystemUI/res/values-sw600dp-port/config.xml
index 857e162..7daad1a 100644
--- a/packages/SystemUI/res/values-sw600dp-port/config.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/config.xml
@@ -24,6 +24,9 @@
<!-- The number of columns in the QuickSettings -->
<integer name="quick_settings_num_columns">3</integer>
+ <!-- The number of columns in the infinite grid QuickSettings -->
+ <integer name="quick_settings_infinite_grid_num_columns">6</integer>
+
<integer name="power_menu_lite_max_columns">2</integer>
<integer name="power_menu_lite_max_rows">3</integer>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 38ef0e9..6f94f9e 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -73,6 +73,9 @@
<!-- The number of columns in the infinite grid QuickSettings -->
<integer name="quick_settings_infinite_grid_num_columns">4</integer>
+ <!-- The number of columns in the Dual Shade QuickSettings -->
+ <integer name="quick_settings_dual_shade_num_columns">4</integer>
+
<!-- Override column number for quick settings.
For now, this value has effect only when flag lockscreen.enable_landscape is enabled.
TODO (b/293252410) - change this comment/resource when flag is enabled -->
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index b2acc2a..3c8bb09 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -48,6 +48,7 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -434,11 +435,13 @@
@Composable
private fun EndSidePanel(searchQuery: String, modifier: Modifier, category: ShortcutCategory?) {
+ val listState = rememberLazyListState()
+ LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) }
if (category == null) {
NoSearchResultsText(horizontalPadding = 24.dp, fillHeight = false)
return
}
- LazyColumn(modifier = modifier) {
+ LazyColumn(modifier = modifier, state = listState) {
items(category.subCategories) { subcategory ->
SubCategoryContainerDualPane(searchQuery = searchQuery, subCategory = subcategory)
Spacer(modifier = Modifier.height(8.dp))
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index 1fe54e4..31e867e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -31,12 +31,12 @@
import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout
import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
-import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModelImpl
import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModelImpl
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModelImpl
+import com.android.systemui.qs.panels.ui.viewmodel.QSColumnsSizeViewModelImpl
+import com.android.systemui.qs.panels.ui.viewmodel.QSColumnsViewModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -55,7 +55,7 @@
@Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
- @Binds fun bindGridSizeViewModel(impl: FixedColumnsSizeViewModelImpl): FixedColumnsSizeViewModel
+ @Binds fun bindQSColumnsViewModel(impl: QSColumnsSizeViewModelImpl): QSColumnsViewModel
@Binds
fun bindIconLabelVisibilityViewModel(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
deleted file mode 100644
index 32ce973..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.data.repository
-
-import com.android.systemui.dagger.SysUISingleton
-import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-@SysUISingleton
-class FixedColumnsRepository @Inject constructor() {
- // Number of columns in the narrowest state for consistency
- private val _columns = MutableStateFlow(4)
- val columns: StateFlow<Int> = _columns.asStateFlow()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepository.kt
new file mode 100644
index 0000000..082f622
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepository.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.util.kotlin.emitOnStart
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class QSColumnsRepository
+@Inject
+constructor(
+ @Application scope: CoroutineScope,
+ @Main private val resources: Resources,
+ configurationRepository: ConfigurationRepository,
+) {
+ val columns: StateFlow<Int> =
+ if (DualShade.isEnabled) {
+ flowOf(resources.getInteger(R.integer.quick_settings_dual_shade_num_columns))
+ } else {
+ configurationRepository.onConfigurationChange.emitOnStart().mapLatest {
+ resources.getInteger(R.integer.quick_settings_infinite_grid_num_columns)
+ }
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ resources.getInteger(R.integer.quick_settings_infinite_grid_num_columns),
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractor.kt
similarity index 84%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractor.kt
index 9591002..9b45c56 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractor.kt
@@ -17,11 +17,11 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.data.repository.FixedColumnsRepository
+import com.android.systemui.qs.panels.data.repository.QSColumnsRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
@SysUISingleton
-class FixedColumnsSizeInteractor @Inject constructor(repo: FixedColumnsRepository) {
+class QSColumnsInteractor @Inject constructor(repo: QSColumnsRepository) {
val columns: StateFlow<Int> = repo.columns
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 3ba49ad..6920e49 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -31,8 +31,8 @@
import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
import com.android.systemui.qs.panels.ui.compose.rememberEditListState
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.QSColumnsViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileSquishinessViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -45,7 +45,7 @@
@Inject
constructor(
private val iconTilesViewModel: IconTilesViewModel,
- private val gridSizeViewModel: FixedColumnsSizeViewModel,
+ private val gridSizeViewModel: QSColumnsViewModel,
private val squishinessViewModel: TileSquishinessViewModel,
) : PaginatableGridLayout {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
index d4f8298..78212b2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -29,13 +29,13 @@
@Inject
constructor(
iconTilesViewModel: IconTilesViewModel,
- gridSizeViewModel: FixedColumnsSizeViewModel,
+ gridSizeViewModel: QSColumnsViewModel,
iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
paginatedGridInteractor: PaginatedGridInteractor,
@Application applicationScope: CoroutineScope,
) :
IconTilesViewModel by iconTilesViewModel,
- FixedColumnsSizeViewModel by gridSizeViewModel,
+ QSColumnsViewModel by gridSizeViewModel,
IconLabelVisibilityViewModel by iconLabelVisibilityViewModel {
val rows =
paginatedGridInteractor.rows.stateIn(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
deleted file mode 100644
index 2049edb..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.ui.viewmodel
-
-import com.android.systemui.dagger.SysUISingleton
-import javax.inject.Inject
-
-@SysUISingleton
-class PartitionedGridViewModel
-@Inject
-constructor(
- iconTilesViewModel: IconTilesViewModel,
- gridSizeViewModel: FixedColumnsSizeViewModel,
- iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
-) :
- IconTilesViewModel by iconTilesViewModel,
- FixedColumnsSizeViewModel by gridSizeViewModel,
- IconLabelVisibilityViewModel by iconLabelVisibilityViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt
index 865c86b..0f1c77e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt
@@ -17,16 +17,16 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.domain.interactor.FixedColumnsSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.QSColumnsInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
-interface FixedColumnsSizeViewModel {
+interface QSColumnsViewModel {
val columns: StateFlow<Int>
}
@SysUISingleton
-class FixedColumnsSizeViewModelImpl @Inject constructor(interactor: FixedColumnsSizeInteractor) :
- FixedColumnsSizeViewModel {
+class QSColumnsSizeViewModelImpl @Inject constructor(interactor: QSColumnsInteractor) :
+ QSColumnsViewModel {
override val columns: StateFlow<Int> = interactor.columns
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
index 88e3019..72b586a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
@@ -40,14 +40,14 @@
@Inject
constructor(
tilesInteractor: CurrentTilesInteractor,
- fixedColumnsSizeViewModel: FixedColumnsSizeViewModel,
+ qsColumnsViewModel: QSColumnsViewModel,
quickQuickSettingsRowInteractor: QuickQuickSettingsRowInteractor,
val squishinessViewModel: TileSquishinessViewModel,
private val iconTilesViewModel: IconTilesViewModel,
@Application private val applicationScope: CoroutineScope,
) {
- val columns = fixedColumnsSizeViewModel.columns
+ val columns = qsColumnsViewModel.columns
private val rows =
quickQuickSettingsRowInteractor.rows.stateIn(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationProgressTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationProgressTemplateViewWrapper.kt
new file mode 100644
index 0000000..a693fd3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationProgressTemplateViewWrapper.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification.row.wrapper
+
+import android.content.Context
+import android.view.View
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+
+/** Wraps a notification containing a progress template */
+class NotificationProgressTemplateViewWrapper(
+ ctx: Context,
+ view: View,
+ row: ExpandableNotificationRow,
+) : NotificationTemplateViewWrapper(ctx, view, row)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index 22b95ef..182fba3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -76,6 +76,8 @@
return new NotificationCompactHeadsUpTemplateViewWrapper(ctx, v, row);
} else if ("compactMessagingHUN".equals((v.getTag()))) {
return new NotificationCompactMessagingTemplateViewWrapper(ctx, v, row);
+ } else if ("progress".equals(v.getTag())) {
+ return new NotificationProgressTemplateViewWrapper(ctx, v, row);
}
if (row.getEntry().getSbn().getNotification().isStyle(
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 6879a34..d85cfcd 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -24,7 +24,7 @@
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
import com.android.systemui.res.R
-import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor
+import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer
@Composable
fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -44,15 +44,15 @@
successResId = R.raw.trackpad_back_success,
),
)
- val gestureMonitorProvider =
- DistanceBasedGestureMonitorProvider(
- monitorFactory = { distanceThresholdPx, gestureStateCallback ->
- BackGestureMonitor(distanceThresholdPx).also {
+ val gestureRecognizerProvider =
+ DistanceBasedGestureRecognizerProvider(
+ recognizerFactory = { distanceThresholdPx, gestureStateCallback ->
+ BackGestureRecognizer(distanceThresholdPx).also {
it.addGestureStateCallback(gestureStateCallback)
}
}
)
- GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
+ GestureTutorialScreen(screenConfig, gestureRecognizerProvider, onDoneButtonClicked, onBack)
}
@Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 72389cd..75c66f2 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -37,39 +37,39 @@
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
-import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
-interface GestureMonitorProvider {
+interface GestureRecognizerProvider {
@Composable
- fun rememberGestureMonitor(
+ fun rememberGestureRecognizer(
resources: Resources,
gestureStateChangedCallback: (GestureState) -> Unit,
- ): TouchpadGestureMonitor
+ ): GestureRecognizer
}
typealias gestureStateCallback = (GestureState) -> Unit
-class DistanceBasedGestureMonitorProvider(
- val monitorFactory: (Int, gestureStateCallback) -> TouchpadGestureMonitor
-) : GestureMonitorProvider {
+class DistanceBasedGestureRecognizerProvider(
+ val recognizerFactory: (Int, gestureStateCallback) -> GestureRecognizer
+) : GestureRecognizerProvider {
@Composable
- override fun rememberGestureMonitor(
+ override fun rememberGestureRecognizer(
resources: Resources,
gestureStateChangedCallback: (GestureState) -> Unit,
- ): TouchpadGestureMonitor {
+ ): GestureRecognizer {
val distanceThresholdPx =
resources.getDimensionPixelSize(
com.android.internal.R.dimen.system_gestures_distance_threshold
)
return remember(distanceThresholdPx) {
- monitorFactory(distanceThresholdPx, gestureStateChangedCallback)
+ recognizerFactory(distanceThresholdPx, gestureStateChangedCallback)
}
}
}
@@ -77,7 +77,7 @@
fun GestureState.toTutorialActionState(): TutorialActionState {
return when (this) {
NotStarted -> TutorialActionState.NotStarted
- is InProgress -> TutorialActionState.InProgress()
+ is InProgress -> TutorialActionState.InProgress(progress)
Finished -> TutorialActionState.Finished
}
}
@@ -85,21 +85,21 @@
@Composable
fun GestureTutorialScreen(
screenConfig: TutorialScreenConfig,
- gestureMonitorProvider: GestureMonitorProvider,
+ gestureRecognizerProvider: GestureRecognizerProvider,
onDoneButtonClicked: () -> Unit,
onBack: () -> Unit,
) {
BackHandler(onBack = onBack)
var gestureState: GestureState by remember { mutableStateOf(NotStarted) }
var easterEggTriggered by remember { mutableStateOf(false) }
- val gestureMonitor =
- gestureMonitorProvider.rememberGestureMonitor(
+ val gestureRecognizer =
+ gestureRecognizerProvider.rememberGestureRecognizer(
resources = LocalContext.current.resources,
gestureStateChangedCallback = { gestureState = it },
)
val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered = true }
val gestureHandler =
- remember(gestureMonitor) { TouchpadGestureHandler(gestureMonitor, easterEggMonitor) }
+ remember(gestureRecognizer) { TouchpadGestureHandler(gestureRecognizer, easterEggMonitor) }
TouchpadGesturesHandlingBox(
gestureHandler,
gestureState,
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index a55fa44..69ec598 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -23,7 +23,7 @@
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
import com.android.systemui.res.R
-import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor
+import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer
@Composable
fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -43,15 +43,15 @@
successResId = R.raw.trackpad_home_success,
),
)
- val gestureMonitorProvider =
- DistanceBasedGestureMonitorProvider(
- monitorFactory = { distanceThresholdPx, gestureStateCallback ->
- HomeGestureMonitor(distanceThresholdPx).also {
+ val gestureRecognizerProvider =
+ DistanceBasedGestureRecognizerProvider(
+ recognizerFactory = { distanceThresholdPx, gestureStateCallback ->
+ HomeGestureRecognizer(distanceThresholdPx).also {
it.addGestureStateCallback(gestureStateCallback)
}
}
)
- GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
+ GestureTutorialScreen(screenConfig, gestureRecognizerProvider, onDoneButtonClicked, onBack)
}
@Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index 6ee15aa..3097a18 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -24,9 +24,9 @@
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
-import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureMonitor
-import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
+import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer
@Composable
fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -46,13 +46,13 @@
successResId = R.raw.trackpad_recent_apps_success,
),
)
- val gestureMonitorProvider =
- object : GestureMonitorProvider {
+ val gestureRecognizerProvider =
+ object : GestureRecognizerProvider {
@Composable
- override fun rememberGestureMonitor(
+ override fun rememberGestureRecognizer(
resources: Resources,
gestureStateChangedCallback: (GestureState) -> Unit,
- ): TouchpadGestureMonitor {
+ ): GestureRecognizer {
val distanceThresholdPx =
resources.getDimensionPixelSize(
com.android.internal.R.dimen.system_gestures_distance_threshold
@@ -60,13 +60,12 @@
val velocityThresholdPxPerMs =
resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold)
return remember(distanceThresholdPx, velocityThresholdPxPerMs) {
- RecentAppsGestureMonitor(distanceThresholdPx, velocityThresholdPxPerMs).also {
- it.addGestureStateCallback(gestureStateChangedCallback)
- }
+ RecentAppsGestureRecognizer(distanceThresholdPx, velocityThresholdPxPerMs)
+ .also { it.addGestureStateCallback(gestureStateChangedCallback) }
}
}
}
- GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
+ GestureTutorialScreen(screenConfig, gestureRecognizerProvider, onDoneButtonClicked, onBack)
}
@Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
rename to packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
index 490f04d..56e97a3 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
@@ -19,8 +19,8 @@
import android.view.MotionEvent
import kotlin.math.abs
-/** Monitors for touchpad back gesture, that is three fingers swiping left or right */
-class BackGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
+/** Recognizes touchpad back gesture, that is three fingers swiping left or right */
+class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer {
private val distanceTracker = DistanceTracker()
private var gestureStateChangedCallback: (GestureState) -> Unit = {}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
rename to packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
index 9216821..d146268 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
@@ -19,8 +19,8 @@
import android.view.MotionEvent
import java.util.function.Consumer
-/** Monitor for touchpad gestures that can notify callback when [GestureState] changes. */
-interface TouchpadGestureMonitor : Consumer<MotionEvent> {
+/** Based on passed [MotionEvent]s recognizes different states of gesture and notifies callback. */
+interface GestureRecognizer : Consumer<MotionEvent> {
fun addGestureStateCallback(callback: (GestureState) -> Unit)
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
rename to packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
index 83d4f56..3db9d7c 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
@@ -18,8 +18,8 @@
import android.view.MotionEvent
-/** Monitors for touchpad home gesture, that is three fingers swiping up */
-class HomeGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
+/** Recognizes touchpad home gesture, that is three fingers swiping up */
+class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer {
private val distanceTracker = DistanceTracker()
private var gestureStateChangedCallback: (GestureState) -> Unit = {}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
rename to packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
index 1731bb8..a194ad6 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
@@ -20,16 +20,16 @@
import kotlin.math.abs
/**
- * Monitors recent apps gesture completion. That is - using three fingers on touchpad - swipe up
- * over some distance threshold and then slow down gesture before fingers are lifted. Implementation
- * is based on [com.android.quickstep.util.TriggerSwipeUpTouchTracker]
+ * Recognizes apps gesture completion. That is - using three fingers on touchpad - swipe up over
+ * some distance threshold and then slow down gesture before fingers are lifted. Implementation is
+ * based on [com.android.quickstep.util.TriggerSwipeUpTouchTracker]
*/
-class RecentAppsGestureMonitor(
+class RecentAppsGestureRecognizer(
private val gestureDistanceThresholdPx: Int,
private val velocityThresholdPxPerMs: Float,
private val distanceTracker: DistanceTracker = DistanceTracker(),
private val velocityTracker: VerticalVelocityTracker = VerticalVelocityTracker(),
-) : TouchpadGestureMonitor {
+) : GestureRecognizer {
private var gestureStateChangedCallback: (GestureState) -> Unit = {}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index 4b82ba1..21e2917 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -25,7 +25,7 @@
* motion events passed to [onMotionEvent] and will filter touchpad events accordingly
*/
class TouchpadGestureHandler(
- private val gestureMonitor: Consumer<MotionEvent>,
+ private val gestureRecognizer: Consumer<MotionEvent>,
private val easterEggGestureMonitor: EasterEggGestureMonitor,
) {
@@ -41,7 +41,7 @@
if (isTwoFingerSwipe(event)) {
easterEggGestureMonitor.processTouchpadEvent(event)
} else {
- gestureMonitor.accept(event)
+ gestureRecognizer.accept(event)
}
true
} else {
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching.json b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching.json
index 60bff17..7f62357 100644
--- a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching.json
+++ b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching.json
@@ -1,25 +1,30 @@
{
"frame_ids": [
- "before",
0,
- 26,
- 52,
- 78,
- 105,
- 131,
- 157,
- 184,
- 210,
- 236,
- 263,
- 289,
- 315,
- 342,
- 368,
- 394,
- 421,
- 447,
- 473,
+ 20,
+ 40,
+ 60,
+ 80,
+ 100,
+ 120,
+ 140,
+ 160,
+ 180,
+ 200,
+ 220,
+ 240,
+ 260,
+ 280,
+ 300,
+ 320,
+ 340,
+ 360,
+ 380,
+ 400,
+ 420,
+ 440,
+ 460,
+ 480,
500
],
"features": [
@@ -28,70 +33,82 @@
"type": "rect",
"data_points": [
{
- "left": 0,
- "top": 0,
- "right": 0,
- "bottom": 0
- },
- {
"left": 100,
"top": 300,
"right": 200,
"bottom": 400
},
{
- "left": 98,
- "top": 293,
- "right": 203,
- "bottom": 407
+ "left": 99,
+ "top": 296,
+ "right": 202,
+ "bottom": 404
},
{
- "left": 91,
- "top": 269,
- "right": 213,
- "bottom": 430
+ "left": 95,
+ "top": 283,
+ "right": 207,
+ "bottom": 417
},
{
- "left": 71,
- "top": 206,
- "right": 240,
- "bottom": 491
+ "left": 86,
+ "top": 256,
+ "right": 219,
+ "bottom": 443
},
{
- "left": 34,
- "top": 98,
- "right": 283,
- "bottom": 595
+ "left": 68,
+ "top": 198,
+ "right": 243,
+ "bottom": 499
},
{
- "left": 22,
- "top": 63,
- "right": 296,
- "bottom": 629
+ "left": 39,
+ "top": 110,
+ "right": 278,
+ "bottom": 584
+ },
+ {
+ "left": 26,
+ "top": 74,
+ "right": 292,
+ "bottom": 618
+ },
+ {
+ "left": 19,
+ "top": 55,
+ "right": 299,
+ "bottom": 637
},
{
"left": 15,
- "top": 44,
- "right": 303,
- "bottom": 648
+ "top": 42,
+ "right": 304,
+ "bottom": 649
},
{
- "left": 11,
- "top": 32,
- "right": 308,
- "bottom": 659
+ "left": 12,
+ "top": 33,
+ "right": 307,
+ "bottom": 658
},
{
- "left": 8,
- "top": 23,
- "right": 311,
- "bottom": 667
+ "left": 9,
+ "top": 27,
+ "right": 310,
+ "bottom": 664
+ },
+ {
+ "left": 7,
+ "top": 21,
+ "right": 312,
+ "bottom": 669
},
{
"left": 6,
- "top": 18,
- "right": 313,
- "bottom": 673
+ "top": 17,
+ "right": 314,
+ "bottom": 674
},
{
"left": 5,
@@ -100,16 +117,22 @@
"bottom": 677
},
{
- "left": 3,
- "top": 9,
+ "left": 4,
+ "top": 10,
"right": 316,
- "bottom": 681
+ "bottom": 680
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 317,
+ "bottom": 682
},
{
"left": 2,
- "top": 7,
- "right": 317,
- "bottom": 683
+ "top": 6,
+ "right": 318,
+ "bottom": 684
},
{
"left": 2,
@@ -119,7 +142,7 @@
},
{
"left": 1,
- "top": 3,
+ "top": 4,
"right": 319,
"bottom": 687
},
@@ -130,6 +153,18 @@
"bottom": 688
},
{
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
"left": 0,
"top": 1,
"right": 320,
@@ -159,7 +194,6 @@
"name": "corner_radii",
"type": "cornerRadii",
"data_points": [
- null,
{
"top_left_x": 10,
"top_left_y": 10,
@@ -171,184 +205,244 @@
"bottom_left_y": 20
},
{
- "top_left_x": 9.762664,
- "top_left_y": 9.762664,
- "top_right_x": 9.762664,
- "top_right_y": 9.762664,
- "bottom_right_x": 19.525328,
- "bottom_right_y": 19.525328,
- "bottom_left_x": 19.525328,
- "bottom_left_y": 19.525328
+ "top_left_x": 9.865689,
+ "top_left_y": 9.865689,
+ "top_right_x": 9.865689,
+ "top_right_y": 9.865689,
+ "bottom_right_x": 19.731379,
+ "bottom_right_y": 19.731379,
+ "bottom_left_x": 19.731379,
+ "bottom_left_y": 19.731379
},
{
- "top_left_x": 8.969244,
- "top_left_y": 8.969244,
- "top_right_x": 8.969244,
- "top_right_y": 8.969244,
- "bottom_right_x": 17.938488,
- "bottom_right_y": 17.938488,
- "bottom_left_x": 17.938488,
- "bottom_left_y": 17.938488
+ "top_left_x": 9.419104,
+ "top_left_y": 9.419104,
+ "top_right_x": 9.419104,
+ "top_right_y": 9.419104,
+ "bottom_right_x": 18.838207,
+ "bottom_right_y": 18.838207,
+ "bottom_left_x": 18.838207,
+ "bottom_left_y": 18.838207
},
{
- "top_left_x": 6.8709626,
- "top_left_y": 6.8709626,
- "top_right_x": 6.8709626,
- "top_right_y": 6.8709626,
- "bottom_right_x": 13.741925,
- "bottom_right_y": 13.741925,
- "bottom_left_x": 13.741925,
- "bottom_left_y": 13.741925
+ "top_left_x": 8.533693,
+ "top_left_y": 8.533693,
+ "top_right_x": 8.533693,
+ "top_right_y": 8.533693,
+ "bottom_right_x": 17.067387,
+ "bottom_right_y": 17.067387,
+ "bottom_left_x": 17.067387,
+ "bottom_left_y": 17.067387
},
{
- "top_left_x": 3.260561,
- "top_left_y": 3.260561,
- "top_right_x": 3.260561,
- "top_right_y": 3.260561,
- "bottom_right_x": 6.521122,
- "bottom_right_y": 6.521122,
- "bottom_left_x": 6.521122,
- "bottom_left_y": 6.521122
+ "top_left_x": 6.5919456,
+ "top_left_y": 6.5919456,
+ "top_right_x": 6.5919456,
+ "top_right_y": 6.5919456,
+ "bottom_right_x": 13.183891,
+ "bottom_right_y": 13.183891,
+ "bottom_left_x": 13.183891,
+ "bottom_left_y": 13.183891
},
{
- "top_left_x": 2.0915751,
- "top_left_y": 2.0915751,
- "top_right_x": 2.0915751,
- "top_right_y": 2.0915751,
- "bottom_right_x": 4.1831503,
- "bottom_right_y": 4.1831503,
- "bottom_left_x": 4.1831503,
- "bottom_left_y": 4.1831503
+ "top_left_x": 3.6674318,
+ "top_left_y": 3.6674318,
+ "top_right_x": 3.6674318,
+ "top_right_y": 3.6674318,
+ "bottom_right_x": 7.3348637,
+ "bottom_right_y": 7.3348637,
+ "bottom_left_x": 7.3348637,
+ "bottom_left_y": 7.3348637
},
{
- "top_left_x": 1.4640827,
- "top_left_y": 1.4640827,
- "top_right_x": 1.4640827,
- "top_right_y": 1.4640827,
- "bottom_right_x": 2.9281654,
- "bottom_right_y": 2.9281654,
- "bottom_left_x": 2.9281654,
- "bottom_left_y": 2.9281654
+ "top_left_x": 2.4832253,
+ "top_left_y": 2.4832253,
+ "top_right_x": 2.4832253,
+ "top_right_y": 2.4832253,
+ "bottom_right_x": 4.9664507,
+ "bottom_right_y": 4.9664507,
+ "bottom_left_x": 4.9664507,
+ "bottom_left_y": 4.9664507
},
{
- "top_left_x": 1.057313,
- "top_left_y": 1.057313,
- "top_right_x": 1.057313,
- "top_right_y": 1.057313,
- "bottom_right_x": 2.114626,
- "bottom_right_y": 2.114626,
- "bottom_left_x": 2.114626,
- "bottom_left_y": 2.114626
+ "top_left_x": 1.8252907,
+ "top_left_y": 1.8252907,
+ "top_right_x": 1.8252907,
+ "top_right_y": 1.8252907,
+ "bottom_right_x": 3.6505814,
+ "bottom_right_y": 3.6505814,
+ "bottom_left_x": 3.6505814,
+ "bottom_left_y": 3.6505814
},
{
- "top_left_x": 0.7824335,
- "top_left_y": 0.7824335,
- "top_right_x": 0.7824335,
- "top_right_y": 0.7824335,
- "bottom_right_x": 1.564867,
- "bottom_right_y": 1.564867,
- "bottom_left_x": 1.564867,
- "bottom_left_y": 1.564867
+ "top_left_x": 1.4077549,
+ "top_left_y": 1.4077549,
+ "top_right_x": 1.4077549,
+ "top_right_y": 1.4077549,
+ "bottom_right_x": 2.8155098,
+ "bottom_right_y": 2.8155098,
+ "bottom_left_x": 2.8155098,
+ "bottom_left_y": 2.8155098
},
{
- "top_left_x": 0.5863056,
- "top_left_y": 0.5863056,
- "top_right_x": 0.5863056,
- "top_right_y": 0.5863056,
- "bottom_right_x": 1.1726112,
- "bottom_right_y": 1.1726112,
- "bottom_left_x": 1.1726112,
- "bottom_left_y": 1.1726112
+ "top_left_x": 1.1067667,
+ "top_left_y": 1.1067667,
+ "top_right_x": 1.1067667,
+ "top_right_y": 1.1067667,
+ "bottom_right_x": 2.2135334,
+ "bottom_right_y": 2.2135334,
+ "bottom_left_x": 2.2135334,
+ "bottom_left_y": 2.2135334
},
{
- "top_left_x": 0.4332962,
- "top_left_y": 0.4332962,
- "top_right_x": 0.4332962,
- "top_right_y": 0.4332962,
- "bottom_right_x": 0.8665924,
- "bottom_right_y": 0.8665924,
- "bottom_left_x": 0.8665924,
- "bottom_left_y": 0.8665924
+ "top_left_x": 0.88593864,
+ "top_left_y": 0.88593864,
+ "top_right_x": 0.88593864,
+ "top_right_y": 0.88593864,
+ "bottom_right_x": 1.7718773,
+ "bottom_right_y": 1.7718773,
+ "bottom_left_x": 1.7718773,
+ "bottom_left_y": 1.7718773
},
{
- "top_left_x": 0.3145876,
- "top_left_y": 0.3145876,
- "top_right_x": 0.3145876,
- "top_right_y": 0.3145876,
- "bottom_right_x": 0.6291752,
- "bottom_right_y": 0.6291752,
- "bottom_left_x": 0.6291752,
- "bottom_left_y": 0.6291752
+ "top_left_x": 0.7069988,
+ "top_left_y": 0.7069988,
+ "top_right_x": 0.7069988,
+ "top_right_y": 0.7069988,
+ "bottom_right_x": 1.4139977,
+ "bottom_right_y": 1.4139977,
+ "bottom_left_x": 1.4139977,
+ "bottom_left_y": 1.4139977
},
{
- "top_left_x": 0.22506618,
- "top_left_y": 0.22506618,
- "top_right_x": 0.22506618,
- "top_right_y": 0.22506618,
- "bottom_right_x": 0.45013237,
- "bottom_right_y": 0.45013237,
- "bottom_left_x": 0.45013237,
- "bottom_left_y": 0.45013237
+ "top_left_x": 0.55613136,
+ "top_left_y": 0.55613136,
+ "top_right_x": 0.55613136,
+ "top_right_y": 0.55613136,
+ "bottom_right_x": 1.1122627,
+ "bottom_right_y": 1.1122627,
+ "bottom_left_x": 1.1122627,
+ "bottom_left_y": 1.1122627
},
{
- "top_left_x": 0.15591621,
- "top_left_y": 0.15591621,
- "top_right_x": 0.15591621,
- "top_right_y": 0.15591621,
- "bottom_right_x": 0.31183243,
- "bottom_right_y": 0.31183243,
- "bottom_left_x": 0.31183243,
- "bottom_left_y": 0.31183243
+ "top_left_x": 0.44889355,
+ "top_left_y": 0.44889355,
+ "top_right_x": 0.44889355,
+ "top_right_y": 0.44889355,
+ "bottom_right_x": 0.8977871,
+ "bottom_right_y": 0.8977871,
+ "bottom_left_x": 0.8977871,
+ "bottom_left_y": 0.8977871
},
{
- "top_left_x": 0.100948334,
- "top_left_y": 0.100948334,
- "top_right_x": 0.100948334,
- "top_right_y": 0.100948334,
- "bottom_right_x": 0.20189667,
- "bottom_right_y": 0.20189667,
- "bottom_left_x": 0.20189667,
- "bottom_left_y": 0.20189667
+ "top_left_x": 0.34557533,
+ "top_left_y": 0.34557533,
+ "top_right_x": 0.34557533,
+ "top_right_y": 0.34557533,
+ "bottom_right_x": 0.69115067,
+ "bottom_right_y": 0.69115067,
+ "bottom_left_x": 0.69115067,
+ "bottom_left_y": 0.69115067
},
{
- "top_left_x": 0.06496239,
- "top_left_y": 0.06496239,
- "top_right_x": 0.06496239,
- "top_right_y": 0.06496239,
- "bottom_right_x": 0.12992477,
- "bottom_right_y": 0.12992477,
- "bottom_left_x": 0.12992477,
- "bottom_left_y": 0.12992477
+ "top_left_x": 0.27671337,
+ "top_left_y": 0.27671337,
+ "top_right_x": 0.27671337,
+ "top_right_y": 0.27671337,
+ "bottom_right_x": 0.55342674,
+ "bottom_right_y": 0.55342674,
+ "bottom_left_x": 0.55342674,
+ "bottom_left_y": 0.55342674
},
{
- "top_left_x": 0.03526497,
- "top_left_y": 0.03526497,
- "top_right_x": 0.03526497,
- "top_right_y": 0.03526497,
- "bottom_right_x": 0.07052994,
- "bottom_right_y": 0.07052994,
- "bottom_left_x": 0.07052994,
- "bottom_left_y": 0.07052994
+ "top_left_x": 0.20785141,
+ "top_left_y": 0.20785141,
+ "top_right_x": 0.20785141,
+ "top_right_y": 0.20785141,
+ "bottom_right_x": 0.41570282,
+ "bottom_right_y": 0.41570282,
+ "bottom_left_x": 0.41570282,
+ "bottom_left_y": 0.41570282
},
{
- "top_left_x": 0.014661789,
- "top_left_y": 0.014661789,
- "top_right_x": 0.014661789,
- "top_right_y": 0.014661789,
- "bottom_right_x": 0.029323578,
- "bottom_right_y": 0.029323578,
- "bottom_left_x": 0.029323578,
- "bottom_left_y": 0.029323578
+ "top_left_x": 0.1601448,
+ "top_left_y": 0.1601448,
+ "top_right_x": 0.1601448,
+ "top_right_y": 0.1601448,
+ "bottom_right_x": 0.3202896,
+ "bottom_right_y": 0.3202896,
+ "bottom_left_x": 0.3202896,
+ "bottom_left_y": 0.3202896
},
{
- "top_left_x": 0.0041856766,
- "top_left_y": 0.0041856766,
- "top_right_x": 0.0041856766,
- "top_right_y": 0.0041856766,
- "bottom_right_x": 0.008371353,
- "bottom_right_y": 0.008371353,
- "bottom_left_x": 0.008371353,
- "bottom_left_y": 0.008371353
+ "top_left_x": 0.117860794,
+ "top_left_y": 0.117860794,
+ "top_right_x": 0.117860794,
+ "top_right_y": 0.117860794,
+ "bottom_right_x": 0.23572159,
+ "bottom_right_y": 0.23572159,
+ "bottom_left_x": 0.23572159,
+ "bottom_left_y": 0.23572159
+ },
+ {
+ "top_left_x": 0.08036041,
+ "top_left_y": 0.08036041,
+ "top_right_x": 0.08036041,
+ "top_right_y": 0.08036041,
+ "bottom_right_x": 0.16072083,
+ "bottom_right_y": 0.16072083,
+ "bottom_left_x": 0.16072083,
+ "bottom_left_y": 0.16072083
+ },
+ {
+ "top_left_x": 0.05836296,
+ "top_left_y": 0.05836296,
+ "top_right_x": 0.05836296,
+ "top_right_y": 0.05836296,
+ "bottom_right_x": 0.11672592,
+ "bottom_right_y": 0.11672592,
+ "bottom_left_x": 0.11672592,
+ "bottom_left_y": 0.11672592
+ },
+ {
+ "top_left_x": 0.03636551,
+ "top_left_y": 0.03636551,
+ "top_right_x": 0.03636551,
+ "top_right_y": 0.03636551,
+ "bottom_right_x": 0.07273102,
+ "bottom_right_y": 0.07273102,
+ "bottom_left_x": 0.07273102,
+ "bottom_left_y": 0.07273102
+ },
+ {
+ "top_left_x": 0.018137932,
+ "top_left_y": 0.018137932,
+ "top_right_x": 0.018137932,
+ "top_right_y": 0.018137932,
+ "bottom_right_x": 0.036275864,
+ "bottom_right_y": 0.036275864,
+ "bottom_left_x": 0.036275864,
+ "bottom_left_y": 0.036275864
+ },
+ {
+ "top_left_x": 0.0082063675,
+ "top_left_y": 0.0082063675,
+ "top_right_x": 0.0082063675,
+ "top_right_y": 0.0082063675,
+ "bottom_right_x": 0.016412735,
+ "bottom_right_y": 0.016412735,
+ "bottom_left_x": 0.016412735,
+ "bottom_left_y": 0.016412735
+ },
+ {
+ "top_left_x": 0.0031013489,
+ "top_left_y": 0.0031013489,
+ "top_right_x": 0.0031013489,
+ "top_right_y": 0.0031013489,
+ "bottom_right_x": 0.0062026978,
+ "bottom_right_y": 0.0062026978,
+ "bottom_left_x": 0.0062026978,
+ "bottom_left_y": 0.0062026978
},
{
"top_left_x": 0,
@@ -367,12 +461,17 @@
"type": "int",
"data_points": [
0,
- 0,
- 115,
- 178,
- 217,
- 241,
- 253,
+ 96,
+ 153,
+ 192,
+ 220,
+ 238,
+ 249,
+ 254,
+ 255,
+ 255,
+ 255,
+ 255,
255,
255,
255,
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching_withSpring.json b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching_withSpring.json
new file mode 100644
index 0000000..18eedd4
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching_withSpring.json
@@ -0,0 +1,375 @@
+{
+ "frame_ids": [
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "type": "rect",
+ "data_points": [
+ {
+ "left": 0,
+ "top": 0,
+ "right": 0,
+ "bottom": 0
+ },
+ {
+ "left": 94,
+ "top": 284,
+ "right": 206,
+ "bottom": 414
+ },
+ {
+ "left": 83,
+ "top": 251,
+ "right": 219,
+ "bottom": 447
+ },
+ {
+ "left": 70,
+ "top": 212,
+ "right": 234,
+ "bottom": 485
+ },
+ {
+ "left": 57,
+ "top": 173,
+ "right": 250,
+ "bottom": 522
+ },
+ {
+ "left": 46,
+ "top": 139,
+ "right": 264,
+ "bottom": 555
+ },
+ {
+ "left": 36,
+ "top": 109,
+ "right": 276,
+ "bottom": 584
+ },
+ {
+ "left": 28,
+ "top": 84,
+ "right": 285,
+ "bottom": 608
+ },
+ {
+ "left": 21,
+ "top": 65,
+ "right": 293,
+ "bottom": 627
+ },
+ {
+ "left": 16,
+ "top": 49,
+ "right": 300,
+ "bottom": 642
+ },
+ {
+ "left": 12,
+ "top": 36,
+ "right": 305,
+ "bottom": 653
+ },
+ {
+ "left": 9,
+ "top": 27,
+ "right": 308,
+ "bottom": 662
+ },
+ {
+ "left": 7,
+ "top": 20,
+ "right": 312,
+ "bottom": 669
+ },
+ {
+ "left": 5,
+ "top": 14,
+ "right": 314,
+ "bottom": 675
+ },
+ {
+ "left": 4,
+ "top": 11,
+ "right": 315,
+ "bottom": 678
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 317,
+ "bottom": 684
+ },
+ {
+ "left": 1,
+ "top": 4,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 318,
+ "bottom": 686
+ },
+ {
+ "left": 0,
+ "top": 2,
+ "right": 319,
+ "bottom": 687
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 9.492916,
+ "top_left_y": 9.492916,
+ "top_right_x": 9.492916,
+ "top_right_y": 9.492916,
+ "bottom_right_x": 18.985832,
+ "bottom_right_y": 18.985832,
+ "bottom_left_x": 18.985832,
+ "bottom_left_y": 18.985832
+ },
+ {
+ "top_left_x": 8.381761,
+ "top_left_y": 8.381761,
+ "top_right_x": 8.381761,
+ "top_right_y": 8.381761,
+ "bottom_right_x": 16.763521,
+ "bottom_right_y": 16.763521,
+ "bottom_left_x": 16.763521,
+ "bottom_left_y": 16.763521
+ },
+ {
+ "top_left_x": 7.07397,
+ "top_left_y": 7.07397,
+ "top_right_x": 7.07397,
+ "top_right_y": 7.07397,
+ "bottom_right_x": 14.14794,
+ "bottom_right_y": 14.14794,
+ "bottom_left_x": 14.14794,
+ "bottom_left_y": 14.14794
+ },
+ {
+ "top_left_x": 5.7880254,
+ "top_left_y": 5.7880254,
+ "top_right_x": 5.7880254,
+ "top_right_y": 5.7880254,
+ "bottom_right_x": 11.576051,
+ "bottom_right_y": 11.576051,
+ "bottom_left_x": 11.576051,
+ "bottom_left_y": 11.576051
+ },
+ {
+ "top_left_x": 4.6295347,
+ "top_left_y": 4.6295347,
+ "top_right_x": 4.6295347,
+ "top_right_y": 4.6295347,
+ "bottom_right_x": 9.259069,
+ "bottom_right_y": 9.259069,
+ "bottom_left_x": 9.259069,
+ "bottom_left_y": 9.259069
+ },
+ {
+ "top_left_x": 3.638935,
+ "top_left_y": 3.638935,
+ "top_right_x": 3.638935,
+ "top_right_y": 3.638935,
+ "bottom_right_x": 7.27787,
+ "bottom_right_y": 7.27787,
+ "bottom_left_x": 7.27787,
+ "bottom_left_y": 7.27787
+ },
+ {
+ "top_left_x": 2.8209057,
+ "top_left_y": 2.8209057,
+ "top_right_x": 2.8209057,
+ "top_right_y": 2.8209057,
+ "bottom_right_x": 5.6418114,
+ "bottom_right_y": 5.6418114,
+ "bottom_left_x": 5.6418114,
+ "bottom_left_y": 5.6418114
+ },
+ {
+ "top_left_x": 2.1620893,
+ "top_left_y": 2.1620893,
+ "top_right_x": 2.1620893,
+ "top_right_y": 2.1620893,
+ "bottom_right_x": 4.3241787,
+ "bottom_right_y": 4.3241787,
+ "bottom_left_x": 4.3241787,
+ "bottom_left_y": 4.3241787
+ },
+ {
+ "top_left_x": 1.6414614,
+ "top_left_y": 1.6414614,
+ "top_right_x": 1.6414614,
+ "top_right_y": 1.6414614,
+ "bottom_right_x": 3.2829227,
+ "bottom_right_y": 3.2829227,
+ "bottom_left_x": 3.2829227,
+ "bottom_left_y": 3.2829227
+ },
+ {
+ "top_left_x": 1.2361269,
+ "top_left_y": 1.2361269,
+ "top_right_x": 1.2361269,
+ "top_right_y": 1.2361269,
+ "bottom_right_x": 2.4722538,
+ "bottom_right_y": 2.4722538,
+ "bottom_left_x": 2.4722538,
+ "bottom_left_y": 2.4722538
+ },
+ {
+ "top_left_x": 0.92435074,
+ "top_left_y": 0.92435074,
+ "top_right_x": 0.92435074,
+ "top_right_y": 0.92435074,
+ "bottom_right_x": 1.8487015,
+ "bottom_right_y": 1.8487015,
+ "bottom_left_x": 1.8487015,
+ "bottom_left_y": 1.8487015
+ },
+ {
+ "top_left_x": 0.68693924,
+ "top_left_y": 0.68693924,
+ "top_right_x": 0.68693924,
+ "top_right_y": 0.68693924,
+ "bottom_right_x": 1.3738785,
+ "bottom_right_y": 1.3738785,
+ "bottom_left_x": 1.3738785,
+ "bottom_left_y": 1.3738785
+ },
+ {
+ "top_left_x": 0.5076904,
+ "top_left_y": 0.5076904,
+ "top_right_x": 0.5076904,
+ "top_right_y": 0.5076904,
+ "bottom_right_x": 1.0153809,
+ "bottom_right_y": 1.0153809,
+ "bottom_left_x": 1.0153809,
+ "bottom_left_y": 1.0153809
+ },
+ {
+ "top_left_x": 0.3733511,
+ "top_left_y": 0.3733511,
+ "top_right_x": 0.3733511,
+ "top_right_y": 0.3733511,
+ "bottom_right_x": 0.7467022,
+ "bottom_right_y": 0.7467022,
+ "bottom_left_x": 0.7467022,
+ "bottom_left_y": 0.7467022
+ },
+ {
+ "top_left_x": 0.27331638,
+ "top_left_y": 0.27331638,
+ "top_right_x": 0.27331638,
+ "top_right_y": 0.27331638,
+ "bottom_right_x": 0.54663277,
+ "bottom_right_y": 0.54663277,
+ "bottom_left_x": 0.54663277,
+ "bottom_left_y": 0.54663277
+ },
+ {
+ "top_left_x": 0.19925308,
+ "top_left_y": 0.19925308,
+ "top_right_x": 0.19925308,
+ "top_right_y": 0.19925308,
+ "bottom_right_x": 0.39850616,
+ "bottom_right_y": 0.39850616,
+ "bottom_left_x": 0.39850616,
+ "bottom_left_y": 0.39850616
+ },
+ {
+ "top_left_x": 0.14470005,
+ "top_left_y": 0.14470005,
+ "top_right_x": 0.14470005,
+ "top_right_y": 0.14470005,
+ "bottom_right_x": 0.2894001,
+ "bottom_right_y": 0.2894001,
+ "bottom_left_x": 0.2894001,
+ "bottom_left_y": 0.2894001
+ },
+ {
+ "top_left_x": 0.10470486,
+ "top_left_y": 0.10470486,
+ "top_right_x": 0.10470486,
+ "top_right_y": 0.10470486,
+ "bottom_right_x": 0.20940971,
+ "bottom_right_y": 0.20940971,
+ "bottom_left_x": 0.20940971,
+ "bottom_left_y": 0.20940971
+ },
+ {
+ "top_left_x": 0.07550812,
+ "top_left_y": 0.07550812,
+ "top_right_x": 0.07550812,
+ "top_right_y": 0.07550812,
+ "bottom_right_x": 0.15101624,
+ "bottom_right_y": 0.15101624,
+ "bottom_left_x": 0.15101624,
+ "bottom_left_y": 0.15101624
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 249,
+ 226,
+ 192,
+ 153,
+ 112,
+ 72,
+ 34,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning.json b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning.json
index ea768c0..98005c5 100644
--- a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning.json
+++ b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning.json
@@ -1,25 +1,30 @@
{
"frame_ids": [
- "before",
0,
- 26,
- 52,
- 78,
- 105,
- 131,
- 157,
- 184,
- 210,
- 236,
- 263,
- 289,
- 315,
- 342,
- 368,
- 394,
- 421,
- 447,
- 473,
+ 20,
+ 40,
+ 60,
+ 80,
+ 100,
+ 120,
+ 140,
+ 160,
+ 180,
+ 200,
+ 220,
+ 240,
+ 260,
+ 280,
+ 300,
+ 320,
+ 340,
+ 360,
+ 380,
+ 400,
+ 420,
+ 440,
+ 460,
+ 480,
500
],
"features": [
@@ -28,70 +33,82 @@
"type": "rect",
"data_points": [
{
- "left": 0,
- "top": 0,
- "right": 0,
- "bottom": 0
- },
- {
"left": 100,
"top": 300,
"right": 200,
"bottom": 400
},
{
- "left": 98,
- "top": 293,
- "right": 203,
- "bottom": 407
+ "left": 99,
+ "top": 296,
+ "right": 202,
+ "bottom": 404
},
{
- "left": 91,
- "top": 269,
- "right": 213,
- "bottom": 430
+ "left": 95,
+ "top": 283,
+ "right": 207,
+ "bottom": 417
},
{
- "left": 71,
- "top": 206,
- "right": 240,
- "bottom": 491
+ "left": 86,
+ "top": 256,
+ "right": 219,
+ "bottom": 443
},
{
- "left": 34,
- "top": 98,
- "right": 283,
- "bottom": 595
+ "left": 68,
+ "top": 198,
+ "right": 243,
+ "bottom": 499
},
{
- "left": 22,
- "top": 63,
- "right": 296,
- "bottom": 629
+ "left": 39,
+ "top": 110,
+ "right": 278,
+ "bottom": 584
+ },
+ {
+ "left": 26,
+ "top": 74,
+ "right": 292,
+ "bottom": 618
+ },
+ {
+ "left": 19,
+ "top": 55,
+ "right": 299,
+ "bottom": 637
},
{
"left": 15,
- "top": 44,
- "right": 303,
- "bottom": 648
+ "top": 42,
+ "right": 304,
+ "bottom": 649
},
{
- "left": 11,
- "top": 32,
- "right": 308,
- "bottom": 659
+ "left": 12,
+ "top": 33,
+ "right": 307,
+ "bottom": 658
},
{
- "left": 8,
- "top": 23,
- "right": 311,
- "bottom": 667
+ "left": 9,
+ "top": 27,
+ "right": 310,
+ "bottom": 664
+ },
+ {
+ "left": 7,
+ "top": 21,
+ "right": 312,
+ "bottom": 669
},
{
"left": 6,
- "top": 18,
- "right": 313,
- "bottom": 673
+ "top": 17,
+ "right": 314,
+ "bottom": 674
},
{
"left": 5,
@@ -100,16 +117,22 @@
"bottom": 677
},
{
- "left": 3,
- "top": 9,
+ "left": 4,
+ "top": 10,
"right": 316,
- "bottom": 681
+ "bottom": 680
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 317,
+ "bottom": 682
},
{
"left": 2,
- "top": 7,
- "right": 317,
- "bottom": 683
+ "top": 6,
+ "right": 318,
+ "bottom": 684
},
{
"left": 2,
@@ -119,7 +142,7 @@
},
{
"left": 1,
- "top": 3,
+ "top": 4,
"right": 319,
"bottom": 687
},
@@ -130,6 +153,18 @@
"bottom": 688
},
{
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
"left": 0,
"top": 1,
"right": 320,
@@ -159,7 +194,6 @@
"name": "corner_radii",
"type": "cornerRadii",
"data_points": [
- null,
{
"top_left_x": 10,
"top_left_y": 10,
@@ -171,184 +205,244 @@
"bottom_left_y": 20
},
{
- "top_left_x": 9.762664,
- "top_left_y": 9.762664,
- "top_right_x": 9.762664,
- "top_right_y": 9.762664,
- "bottom_right_x": 19.525328,
- "bottom_right_y": 19.525328,
- "bottom_left_x": 19.525328,
- "bottom_left_y": 19.525328
+ "top_left_x": 9.865689,
+ "top_left_y": 9.865689,
+ "top_right_x": 9.865689,
+ "top_right_y": 9.865689,
+ "bottom_right_x": 19.731379,
+ "bottom_right_y": 19.731379,
+ "bottom_left_x": 19.731379,
+ "bottom_left_y": 19.731379
},
{
- "top_left_x": 8.969244,
- "top_left_y": 8.969244,
- "top_right_x": 8.969244,
- "top_right_y": 8.969244,
- "bottom_right_x": 17.938488,
- "bottom_right_y": 17.938488,
- "bottom_left_x": 17.938488,
- "bottom_left_y": 17.938488
+ "top_left_x": 9.419104,
+ "top_left_y": 9.419104,
+ "top_right_x": 9.419104,
+ "top_right_y": 9.419104,
+ "bottom_right_x": 18.838207,
+ "bottom_right_y": 18.838207,
+ "bottom_left_x": 18.838207,
+ "bottom_left_y": 18.838207
},
{
- "top_left_x": 6.8709626,
- "top_left_y": 6.8709626,
- "top_right_x": 6.8709626,
- "top_right_y": 6.8709626,
- "bottom_right_x": 13.741925,
- "bottom_right_y": 13.741925,
- "bottom_left_x": 13.741925,
- "bottom_left_y": 13.741925
+ "top_left_x": 8.533693,
+ "top_left_y": 8.533693,
+ "top_right_x": 8.533693,
+ "top_right_y": 8.533693,
+ "bottom_right_x": 17.067387,
+ "bottom_right_y": 17.067387,
+ "bottom_left_x": 17.067387,
+ "bottom_left_y": 17.067387
},
{
- "top_left_x": 3.260561,
- "top_left_y": 3.260561,
- "top_right_x": 3.260561,
- "top_right_y": 3.260561,
- "bottom_right_x": 6.521122,
- "bottom_right_y": 6.521122,
- "bottom_left_x": 6.521122,
- "bottom_left_y": 6.521122
+ "top_left_x": 6.5919456,
+ "top_left_y": 6.5919456,
+ "top_right_x": 6.5919456,
+ "top_right_y": 6.5919456,
+ "bottom_right_x": 13.183891,
+ "bottom_right_y": 13.183891,
+ "bottom_left_x": 13.183891,
+ "bottom_left_y": 13.183891
},
{
- "top_left_x": 2.0915751,
- "top_left_y": 2.0915751,
- "top_right_x": 2.0915751,
- "top_right_y": 2.0915751,
- "bottom_right_x": 4.1831503,
- "bottom_right_y": 4.1831503,
- "bottom_left_x": 4.1831503,
- "bottom_left_y": 4.1831503
+ "top_left_x": 3.6674318,
+ "top_left_y": 3.6674318,
+ "top_right_x": 3.6674318,
+ "top_right_y": 3.6674318,
+ "bottom_right_x": 7.3348637,
+ "bottom_right_y": 7.3348637,
+ "bottom_left_x": 7.3348637,
+ "bottom_left_y": 7.3348637
},
{
- "top_left_x": 1.4640827,
- "top_left_y": 1.4640827,
- "top_right_x": 1.4640827,
- "top_right_y": 1.4640827,
- "bottom_right_x": 2.9281654,
- "bottom_right_y": 2.9281654,
- "bottom_left_x": 2.9281654,
- "bottom_left_y": 2.9281654
+ "top_left_x": 2.4832253,
+ "top_left_y": 2.4832253,
+ "top_right_x": 2.4832253,
+ "top_right_y": 2.4832253,
+ "bottom_right_x": 4.9664507,
+ "bottom_right_y": 4.9664507,
+ "bottom_left_x": 4.9664507,
+ "bottom_left_y": 4.9664507
},
{
- "top_left_x": 1.057313,
- "top_left_y": 1.057313,
- "top_right_x": 1.057313,
- "top_right_y": 1.057313,
- "bottom_right_x": 2.114626,
- "bottom_right_y": 2.114626,
- "bottom_left_x": 2.114626,
- "bottom_left_y": 2.114626
+ "top_left_x": 1.8252907,
+ "top_left_y": 1.8252907,
+ "top_right_x": 1.8252907,
+ "top_right_y": 1.8252907,
+ "bottom_right_x": 3.6505814,
+ "bottom_right_y": 3.6505814,
+ "bottom_left_x": 3.6505814,
+ "bottom_left_y": 3.6505814
},
{
- "top_left_x": 0.7824335,
- "top_left_y": 0.7824335,
- "top_right_x": 0.7824335,
- "top_right_y": 0.7824335,
- "bottom_right_x": 1.564867,
- "bottom_right_y": 1.564867,
- "bottom_left_x": 1.564867,
- "bottom_left_y": 1.564867
+ "top_left_x": 1.4077549,
+ "top_left_y": 1.4077549,
+ "top_right_x": 1.4077549,
+ "top_right_y": 1.4077549,
+ "bottom_right_x": 2.8155098,
+ "bottom_right_y": 2.8155098,
+ "bottom_left_x": 2.8155098,
+ "bottom_left_y": 2.8155098
},
{
- "top_left_x": 0.5863056,
- "top_left_y": 0.5863056,
- "top_right_x": 0.5863056,
- "top_right_y": 0.5863056,
- "bottom_right_x": 1.1726112,
- "bottom_right_y": 1.1726112,
- "bottom_left_x": 1.1726112,
- "bottom_left_y": 1.1726112
+ "top_left_x": 1.1067667,
+ "top_left_y": 1.1067667,
+ "top_right_x": 1.1067667,
+ "top_right_y": 1.1067667,
+ "bottom_right_x": 2.2135334,
+ "bottom_right_y": 2.2135334,
+ "bottom_left_x": 2.2135334,
+ "bottom_left_y": 2.2135334
},
{
- "top_left_x": 0.4332962,
- "top_left_y": 0.4332962,
- "top_right_x": 0.4332962,
- "top_right_y": 0.4332962,
- "bottom_right_x": 0.8665924,
- "bottom_right_y": 0.8665924,
- "bottom_left_x": 0.8665924,
- "bottom_left_y": 0.8665924
+ "top_left_x": 0.88593864,
+ "top_left_y": 0.88593864,
+ "top_right_x": 0.88593864,
+ "top_right_y": 0.88593864,
+ "bottom_right_x": 1.7718773,
+ "bottom_right_y": 1.7718773,
+ "bottom_left_x": 1.7718773,
+ "bottom_left_y": 1.7718773
},
{
- "top_left_x": 0.3145876,
- "top_left_y": 0.3145876,
- "top_right_x": 0.3145876,
- "top_right_y": 0.3145876,
- "bottom_right_x": 0.6291752,
- "bottom_right_y": 0.6291752,
- "bottom_left_x": 0.6291752,
- "bottom_left_y": 0.6291752
+ "top_left_x": 0.7069988,
+ "top_left_y": 0.7069988,
+ "top_right_x": 0.7069988,
+ "top_right_y": 0.7069988,
+ "bottom_right_x": 1.4139977,
+ "bottom_right_y": 1.4139977,
+ "bottom_left_x": 1.4139977,
+ "bottom_left_y": 1.4139977
},
{
- "top_left_x": 0.22506618,
- "top_left_y": 0.22506618,
- "top_right_x": 0.22506618,
- "top_right_y": 0.22506618,
- "bottom_right_x": 0.45013237,
- "bottom_right_y": 0.45013237,
- "bottom_left_x": 0.45013237,
- "bottom_left_y": 0.45013237
+ "top_left_x": 0.55613136,
+ "top_left_y": 0.55613136,
+ "top_right_x": 0.55613136,
+ "top_right_y": 0.55613136,
+ "bottom_right_x": 1.1122627,
+ "bottom_right_y": 1.1122627,
+ "bottom_left_x": 1.1122627,
+ "bottom_left_y": 1.1122627
},
{
- "top_left_x": 0.15591621,
- "top_left_y": 0.15591621,
- "top_right_x": 0.15591621,
- "top_right_y": 0.15591621,
- "bottom_right_x": 0.31183243,
- "bottom_right_y": 0.31183243,
- "bottom_left_x": 0.31183243,
- "bottom_left_y": 0.31183243
+ "top_left_x": 0.44889355,
+ "top_left_y": 0.44889355,
+ "top_right_x": 0.44889355,
+ "top_right_y": 0.44889355,
+ "bottom_right_x": 0.8977871,
+ "bottom_right_y": 0.8977871,
+ "bottom_left_x": 0.8977871,
+ "bottom_left_y": 0.8977871
},
{
- "top_left_x": 0.100948334,
- "top_left_y": 0.100948334,
- "top_right_x": 0.100948334,
- "top_right_y": 0.100948334,
- "bottom_right_x": 0.20189667,
- "bottom_right_y": 0.20189667,
- "bottom_left_x": 0.20189667,
- "bottom_left_y": 0.20189667
+ "top_left_x": 0.34557533,
+ "top_left_y": 0.34557533,
+ "top_right_x": 0.34557533,
+ "top_right_y": 0.34557533,
+ "bottom_right_x": 0.69115067,
+ "bottom_right_y": 0.69115067,
+ "bottom_left_x": 0.69115067,
+ "bottom_left_y": 0.69115067
},
{
- "top_left_x": 0.06496239,
- "top_left_y": 0.06496239,
- "top_right_x": 0.06496239,
- "top_right_y": 0.06496239,
- "bottom_right_x": 0.12992477,
- "bottom_right_y": 0.12992477,
- "bottom_left_x": 0.12992477,
- "bottom_left_y": 0.12992477
+ "top_left_x": 0.27671337,
+ "top_left_y": 0.27671337,
+ "top_right_x": 0.27671337,
+ "top_right_y": 0.27671337,
+ "bottom_right_x": 0.55342674,
+ "bottom_right_y": 0.55342674,
+ "bottom_left_x": 0.55342674,
+ "bottom_left_y": 0.55342674
},
{
- "top_left_x": 0.03526497,
- "top_left_y": 0.03526497,
- "top_right_x": 0.03526497,
- "top_right_y": 0.03526497,
- "bottom_right_x": 0.07052994,
- "bottom_right_y": 0.07052994,
- "bottom_left_x": 0.07052994,
- "bottom_left_y": 0.07052994
+ "top_left_x": 0.20785141,
+ "top_left_y": 0.20785141,
+ "top_right_x": 0.20785141,
+ "top_right_y": 0.20785141,
+ "bottom_right_x": 0.41570282,
+ "bottom_right_y": 0.41570282,
+ "bottom_left_x": 0.41570282,
+ "bottom_left_y": 0.41570282
},
{
- "top_left_x": 0.014661789,
- "top_left_y": 0.014661789,
- "top_right_x": 0.014661789,
- "top_right_y": 0.014661789,
- "bottom_right_x": 0.029323578,
- "bottom_right_y": 0.029323578,
- "bottom_left_x": 0.029323578,
- "bottom_left_y": 0.029323578
+ "top_left_x": 0.1601448,
+ "top_left_y": 0.1601448,
+ "top_right_x": 0.1601448,
+ "top_right_y": 0.1601448,
+ "bottom_right_x": 0.3202896,
+ "bottom_right_y": 0.3202896,
+ "bottom_left_x": 0.3202896,
+ "bottom_left_y": 0.3202896
},
{
- "top_left_x": 0.0041856766,
- "top_left_y": 0.0041856766,
- "top_right_x": 0.0041856766,
- "top_right_y": 0.0041856766,
- "bottom_right_x": 0.008371353,
- "bottom_right_y": 0.008371353,
- "bottom_left_x": 0.008371353,
- "bottom_left_y": 0.008371353
+ "top_left_x": 0.117860794,
+ "top_left_y": 0.117860794,
+ "top_right_x": 0.117860794,
+ "top_right_y": 0.117860794,
+ "bottom_right_x": 0.23572159,
+ "bottom_right_y": 0.23572159,
+ "bottom_left_x": 0.23572159,
+ "bottom_left_y": 0.23572159
+ },
+ {
+ "top_left_x": 0.08036041,
+ "top_left_y": 0.08036041,
+ "top_right_x": 0.08036041,
+ "top_right_y": 0.08036041,
+ "bottom_right_x": 0.16072083,
+ "bottom_right_y": 0.16072083,
+ "bottom_left_x": 0.16072083,
+ "bottom_left_y": 0.16072083
+ },
+ {
+ "top_left_x": 0.05836296,
+ "top_left_y": 0.05836296,
+ "top_right_x": 0.05836296,
+ "top_right_y": 0.05836296,
+ "bottom_right_x": 0.11672592,
+ "bottom_right_y": 0.11672592,
+ "bottom_left_x": 0.11672592,
+ "bottom_left_y": 0.11672592
+ },
+ {
+ "top_left_x": 0.03636551,
+ "top_left_y": 0.03636551,
+ "top_right_x": 0.03636551,
+ "top_right_y": 0.03636551,
+ "bottom_right_x": 0.07273102,
+ "bottom_right_y": 0.07273102,
+ "bottom_left_x": 0.07273102,
+ "bottom_left_y": 0.07273102
+ },
+ {
+ "top_left_x": 0.018137932,
+ "top_left_y": 0.018137932,
+ "top_right_x": 0.018137932,
+ "top_right_y": 0.018137932,
+ "bottom_right_x": 0.036275864,
+ "bottom_right_y": 0.036275864,
+ "bottom_left_x": 0.036275864,
+ "bottom_left_y": 0.036275864
+ },
+ {
+ "top_left_x": 0.0082063675,
+ "top_left_y": 0.0082063675,
+ "top_right_x": 0.0082063675,
+ "top_right_y": 0.0082063675,
+ "bottom_right_x": 0.016412735,
+ "bottom_right_y": 0.016412735,
+ "bottom_left_x": 0.016412735,
+ "bottom_left_y": 0.016412735
+ },
+ {
+ "top_left_x": 0.0031013489,
+ "top_left_y": 0.0031013489,
+ "top_right_x": 0.0031013489,
+ "top_right_y": 0.0031013489,
+ "bottom_right_x": 0.0062026978,
+ "bottom_right_y": 0.0062026978,
+ "bottom_left_x": 0.0062026978,
+ "bottom_left_y": 0.0062026978
},
{
"top_left_x": 0,
@@ -366,20 +460,25 @@
"name": "alpha",
"type": "int",
"data_points": [
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 233,
+ 191,
+ 153,
+ 117,
+ 85,
+ 57,
+ 33,
+ 14,
+ 3,
0,
- 255,
- 255,
- 255,
- 255,
- 255,
- 255,
- 239,
- 183,
- 135,
- 91,
- 53,
- 23,
- 5,
+ 0,
0,
0,
0,
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning_withSpring.json b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning_withSpring.json
new file mode 100644
index 0000000..18eedd4
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning_withSpring.json
@@ -0,0 +1,375 @@
+{
+ "frame_ids": [
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "type": "rect",
+ "data_points": [
+ {
+ "left": 0,
+ "top": 0,
+ "right": 0,
+ "bottom": 0
+ },
+ {
+ "left": 94,
+ "top": 284,
+ "right": 206,
+ "bottom": 414
+ },
+ {
+ "left": 83,
+ "top": 251,
+ "right": 219,
+ "bottom": 447
+ },
+ {
+ "left": 70,
+ "top": 212,
+ "right": 234,
+ "bottom": 485
+ },
+ {
+ "left": 57,
+ "top": 173,
+ "right": 250,
+ "bottom": 522
+ },
+ {
+ "left": 46,
+ "top": 139,
+ "right": 264,
+ "bottom": 555
+ },
+ {
+ "left": 36,
+ "top": 109,
+ "right": 276,
+ "bottom": 584
+ },
+ {
+ "left": 28,
+ "top": 84,
+ "right": 285,
+ "bottom": 608
+ },
+ {
+ "left": 21,
+ "top": 65,
+ "right": 293,
+ "bottom": 627
+ },
+ {
+ "left": 16,
+ "top": 49,
+ "right": 300,
+ "bottom": 642
+ },
+ {
+ "left": 12,
+ "top": 36,
+ "right": 305,
+ "bottom": 653
+ },
+ {
+ "left": 9,
+ "top": 27,
+ "right": 308,
+ "bottom": 662
+ },
+ {
+ "left": 7,
+ "top": 20,
+ "right": 312,
+ "bottom": 669
+ },
+ {
+ "left": 5,
+ "top": 14,
+ "right": 314,
+ "bottom": 675
+ },
+ {
+ "left": 4,
+ "top": 11,
+ "right": 315,
+ "bottom": 678
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 317,
+ "bottom": 684
+ },
+ {
+ "left": 1,
+ "top": 4,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 318,
+ "bottom": 686
+ },
+ {
+ "left": 0,
+ "top": 2,
+ "right": 319,
+ "bottom": 687
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 9.492916,
+ "top_left_y": 9.492916,
+ "top_right_x": 9.492916,
+ "top_right_y": 9.492916,
+ "bottom_right_x": 18.985832,
+ "bottom_right_y": 18.985832,
+ "bottom_left_x": 18.985832,
+ "bottom_left_y": 18.985832
+ },
+ {
+ "top_left_x": 8.381761,
+ "top_left_y": 8.381761,
+ "top_right_x": 8.381761,
+ "top_right_y": 8.381761,
+ "bottom_right_x": 16.763521,
+ "bottom_right_y": 16.763521,
+ "bottom_left_x": 16.763521,
+ "bottom_left_y": 16.763521
+ },
+ {
+ "top_left_x": 7.07397,
+ "top_left_y": 7.07397,
+ "top_right_x": 7.07397,
+ "top_right_y": 7.07397,
+ "bottom_right_x": 14.14794,
+ "bottom_right_y": 14.14794,
+ "bottom_left_x": 14.14794,
+ "bottom_left_y": 14.14794
+ },
+ {
+ "top_left_x": 5.7880254,
+ "top_left_y": 5.7880254,
+ "top_right_x": 5.7880254,
+ "top_right_y": 5.7880254,
+ "bottom_right_x": 11.576051,
+ "bottom_right_y": 11.576051,
+ "bottom_left_x": 11.576051,
+ "bottom_left_y": 11.576051
+ },
+ {
+ "top_left_x": 4.6295347,
+ "top_left_y": 4.6295347,
+ "top_right_x": 4.6295347,
+ "top_right_y": 4.6295347,
+ "bottom_right_x": 9.259069,
+ "bottom_right_y": 9.259069,
+ "bottom_left_x": 9.259069,
+ "bottom_left_y": 9.259069
+ },
+ {
+ "top_left_x": 3.638935,
+ "top_left_y": 3.638935,
+ "top_right_x": 3.638935,
+ "top_right_y": 3.638935,
+ "bottom_right_x": 7.27787,
+ "bottom_right_y": 7.27787,
+ "bottom_left_x": 7.27787,
+ "bottom_left_y": 7.27787
+ },
+ {
+ "top_left_x": 2.8209057,
+ "top_left_y": 2.8209057,
+ "top_right_x": 2.8209057,
+ "top_right_y": 2.8209057,
+ "bottom_right_x": 5.6418114,
+ "bottom_right_y": 5.6418114,
+ "bottom_left_x": 5.6418114,
+ "bottom_left_y": 5.6418114
+ },
+ {
+ "top_left_x": 2.1620893,
+ "top_left_y": 2.1620893,
+ "top_right_x": 2.1620893,
+ "top_right_y": 2.1620893,
+ "bottom_right_x": 4.3241787,
+ "bottom_right_y": 4.3241787,
+ "bottom_left_x": 4.3241787,
+ "bottom_left_y": 4.3241787
+ },
+ {
+ "top_left_x": 1.6414614,
+ "top_left_y": 1.6414614,
+ "top_right_x": 1.6414614,
+ "top_right_y": 1.6414614,
+ "bottom_right_x": 3.2829227,
+ "bottom_right_y": 3.2829227,
+ "bottom_left_x": 3.2829227,
+ "bottom_left_y": 3.2829227
+ },
+ {
+ "top_left_x": 1.2361269,
+ "top_left_y": 1.2361269,
+ "top_right_x": 1.2361269,
+ "top_right_y": 1.2361269,
+ "bottom_right_x": 2.4722538,
+ "bottom_right_y": 2.4722538,
+ "bottom_left_x": 2.4722538,
+ "bottom_left_y": 2.4722538
+ },
+ {
+ "top_left_x": 0.92435074,
+ "top_left_y": 0.92435074,
+ "top_right_x": 0.92435074,
+ "top_right_y": 0.92435074,
+ "bottom_right_x": 1.8487015,
+ "bottom_right_y": 1.8487015,
+ "bottom_left_x": 1.8487015,
+ "bottom_left_y": 1.8487015
+ },
+ {
+ "top_left_x": 0.68693924,
+ "top_left_y": 0.68693924,
+ "top_right_x": 0.68693924,
+ "top_right_y": 0.68693924,
+ "bottom_right_x": 1.3738785,
+ "bottom_right_y": 1.3738785,
+ "bottom_left_x": 1.3738785,
+ "bottom_left_y": 1.3738785
+ },
+ {
+ "top_left_x": 0.5076904,
+ "top_left_y": 0.5076904,
+ "top_right_x": 0.5076904,
+ "top_right_y": 0.5076904,
+ "bottom_right_x": 1.0153809,
+ "bottom_right_y": 1.0153809,
+ "bottom_left_x": 1.0153809,
+ "bottom_left_y": 1.0153809
+ },
+ {
+ "top_left_x": 0.3733511,
+ "top_left_y": 0.3733511,
+ "top_right_x": 0.3733511,
+ "top_right_y": 0.3733511,
+ "bottom_right_x": 0.7467022,
+ "bottom_right_y": 0.7467022,
+ "bottom_left_x": 0.7467022,
+ "bottom_left_y": 0.7467022
+ },
+ {
+ "top_left_x": 0.27331638,
+ "top_left_y": 0.27331638,
+ "top_right_x": 0.27331638,
+ "top_right_y": 0.27331638,
+ "bottom_right_x": 0.54663277,
+ "bottom_right_y": 0.54663277,
+ "bottom_left_x": 0.54663277,
+ "bottom_left_y": 0.54663277
+ },
+ {
+ "top_left_x": 0.19925308,
+ "top_left_y": 0.19925308,
+ "top_right_x": 0.19925308,
+ "top_right_y": 0.19925308,
+ "bottom_right_x": 0.39850616,
+ "bottom_right_y": 0.39850616,
+ "bottom_left_x": 0.39850616,
+ "bottom_left_y": 0.39850616
+ },
+ {
+ "top_left_x": 0.14470005,
+ "top_left_y": 0.14470005,
+ "top_right_x": 0.14470005,
+ "top_right_y": 0.14470005,
+ "bottom_right_x": 0.2894001,
+ "bottom_right_y": 0.2894001,
+ "bottom_left_x": 0.2894001,
+ "bottom_left_y": 0.2894001
+ },
+ {
+ "top_left_x": 0.10470486,
+ "top_left_y": 0.10470486,
+ "top_right_x": 0.10470486,
+ "top_right_y": 0.10470486,
+ "bottom_right_x": 0.20940971,
+ "bottom_right_y": 0.20940971,
+ "bottom_left_x": 0.20940971,
+ "bottom_left_y": 0.20940971
+ },
+ {
+ "top_left_x": 0.07550812,
+ "top_left_y": 0.07550812,
+ "top_right_x": 0.07550812,
+ "top_right_y": 0.07550812,
+ "bottom_right_x": 0.15101624,
+ "bottom_right_y": 0.15101624,
+ "bottom_left_x": 0.15101624,
+ "bottom_left_y": 0.15101624
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 249,
+ 226,
+ 192,
+ 153,
+ 112,
+ 72,
+ 34,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching.json b/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching.json
index 608e633..aa80445 100644
--- a/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching.json
+++ b/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching.json
@@ -1,25 +1,30 @@
{
"frame_ids": [
- "before",
0,
- 26,
- 52,
- 78,
- 105,
- 131,
- 157,
- 184,
- 210,
- 236,
- 263,
- 289,
- 315,
- 342,
- 368,
- 394,
- 421,
- 447,
- 473,
+ 20,
+ 40,
+ 60,
+ 80,
+ 100,
+ 120,
+ 140,
+ 160,
+ 180,
+ 200,
+ 220,
+ 240,
+ 260,
+ 280,
+ 300,
+ 320,
+ 340,
+ 360,
+ 380,
+ 400,
+ 420,
+ 440,
+ 460,
+ 480,
500
],
"features": [
@@ -28,70 +33,82 @@
"type": "rect",
"data_points": [
{
- "left": 0,
- "top": 0,
- "right": 0,
- "bottom": 0
- },
- {
"left": 100,
"top": 300,
"right": 200,
"bottom": 400
},
{
- "left": 98,
- "top": 293,
- "right": 203,
- "bottom": 407
+ "left": 99,
+ "top": 296,
+ "right": 202,
+ "bottom": 404
},
{
- "left": 91,
- "top": 269,
- "right": 213,
- "bottom": 430
+ "left": 95,
+ "top": 283,
+ "right": 207,
+ "bottom": 417
},
{
- "left": 71,
- "top": 206,
- "right": 240,
- "bottom": 491
+ "left": 86,
+ "top": 256,
+ "right": 219,
+ "bottom": 443
},
{
- "left": 34,
- "top": 98,
- "right": 283,
- "bottom": 595
+ "left": 68,
+ "top": 198,
+ "right": 243,
+ "bottom": 499
},
{
- "left": 22,
- "top": 63,
- "right": 296,
- "bottom": 629
+ "left": 39,
+ "top": 110,
+ "right": 278,
+ "bottom": 584
+ },
+ {
+ "left": 26,
+ "top": 74,
+ "right": 292,
+ "bottom": 618
+ },
+ {
+ "left": 19,
+ "top": 55,
+ "right": 299,
+ "bottom": 637
},
{
"left": 15,
- "top": 44,
- "right": 303,
- "bottom": 648
+ "top": 42,
+ "right": 304,
+ "bottom": 649
},
{
- "left": 11,
- "top": 32,
- "right": 308,
- "bottom": 659
+ "left": 12,
+ "top": 33,
+ "right": 307,
+ "bottom": 658
},
{
- "left": 8,
- "top": 23,
- "right": 311,
- "bottom": 667
+ "left": 9,
+ "top": 27,
+ "right": 310,
+ "bottom": 664
+ },
+ {
+ "left": 7,
+ "top": 21,
+ "right": 312,
+ "bottom": 669
},
{
"left": 6,
- "top": 18,
- "right": 313,
- "bottom": 673
+ "top": 17,
+ "right": 314,
+ "bottom": 674
},
{
"left": 5,
@@ -100,16 +117,22 @@
"bottom": 677
},
{
- "left": 3,
- "top": 9,
+ "left": 4,
+ "top": 10,
"right": 316,
- "bottom": 681
+ "bottom": 680
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 317,
+ "bottom": 682
},
{
"left": 2,
- "top": 7,
- "right": 317,
- "bottom": 683
+ "top": 6,
+ "right": 318,
+ "bottom": 684
},
{
"left": 2,
@@ -119,7 +142,7 @@
},
{
"left": 1,
- "top": 3,
+ "top": 4,
"right": 319,
"bottom": 687
},
@@ -130,6 +153,18 @@
"bottom": 688
},
{
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
"left": 0,
"top": 1,
"right": 320,
@@ -159,7 +194,6 @@
"name": "corner_radii",
"type": "cornerRadii",
"data_points": [
- null,
{
"top_left_x": 10,
"top_left_y": 10,
@@ -171,184 +205,244 @@
"bottom_left_y": 20
},
{
- "top_left_x": 9.762664,
- "top_left_y": 9.762664,
- "top_right_x": 9.762664,
- "top_right_y": 9.762664,
- "bottom_right_x": 19.525328,
- "bottom_right_y": 19.525328,
- "bottom_left_x": 19.525328,
- "bottom_left_y": 19.525328
+ "top_left_x": 9.865689,
+ "top_left_y": 9.865689,
+ "top_right_x": 9.865689,
+ "top_right_y": 9.865689,
+ "bottom_right_x": 19.731379,
+ "bottom_right_y": 19.731379,
+ "bottom_left_x": 19.731379,
+ "bottom_left_y": 19.731379
},
{
- "top_left_x": 8.969244,
- "top_left_y": 8.969244,
- "top_right_x": 8.969244,
- "top_right_y": 8.969244,
- "bottom_right_x": 17.938488,
- "bottom_right_y": 17.938488,
- "bottom_left_x": 17.938488,
- "bottom_left_y": 17.938488
+ "top_left_x": 9.419104,
+ "top_left_y": 9.419104,
+ "top_right_x": 9.419104,
+ "top_right_y": 9.419104,
+ "bottom_right_x": 18.838207,
+ "bottom_right_y": 18.838207,
+ "bottom_left_x": 18.838207,
+ "bottom_left_y": 18.838207
},
{
- "top_left_x": 6.8709626,
- "top_left_y": 6.8709626,
- "top_right_x": 6.8709626,
- "top_right_y": 6.8709626,
- "bottom_right_x": 13.741925,
- "bottom_right_y": 13.741925,
- "bottom_left_x": 13.741925,
- "bottom_left_y": 13.741925
+ "top_left_x": 8.533693,
+ "top_left_y": 8.533693,
+ "top_right_x": 8.533693,
+ "top_right_y": 8.533693,
+ "bottom_right_x": 17.067387,
+ "bottom_right_y": 17.067387,
+ "bottom_left_x": 17.067387,
+ "bottom_left_y": 17.067387
},
{
- "top_left_x": 3.260561,
- "top_left_y": 3.260561,
- "top_right_x": 3.260561,
- "top_right_y": 3.260561,
- "bottom_right_x": 6.521122,
- "bottom_right_y": 6.521122,
- "bottom_left_x": 6.521122,
- "bottom_left_y": 6.521122
+ "top_left_x": 6.5919456,
+ "top_left_y": 6.5919456,
+ "top_right_x": 6.5919456,
+ "top_right_y": 6.5919456,
+ "bottom_right_x": 13.183891,
+ "bottom_right_y": 13.183891,
+ "bottom_left_x": 13.183891,
+ "bottom_left_y": 13.183891
},
{
- "top_left_x": 2.0915751,
- "top_left_y": 2.0915751,
- "top_right_x": 2.0915751,
- "top_right_y": 2.0915751,
- "bottom_right_x": 4.1831503,
- "bottom_right_y": 4.1831503,
- "bottom_left_x": 4.1831503,
- "bottom_left_y": 4.1831503
+ "top_left_x": 3.6674318,
+ "top_left_y": 3.6674318,
+ "top_right_x": 3.6674318,
+ "top_right_y": 3.6674318,
+ "bottom_right_x": 7.3348637,
+ "bottom_right_y": 7.3348637,
+ "bottom_left_x": 7.3348637,
+ "bottom_left_y": 7.3348637
},
{
- "top_left_x": 1.4640827,
- "top_left_y": 1.4640827,
- "top_right_x": 1.4640827,
- "top_right_y": 1.4640827,
- "bottom_right_x": 2.9281654,
- "bottom_right_y": 2.9281654,
- "bottom_left_x": 2.9281654,
- "bottom_left_y": 2.9281654
+ "top_left_x": 2.4832253,
+ "top_left_y": 2.4832253,
+ "top_right_x": 2.4832253,
+ "top_right_y": 2.4832253,
+ "bottom_right_x": 4.9664507,
+ "bottom_right_y": 4.9664507,
+ "bottom_left_x": 4.9664507,
+ "bottom_left_y": 4.9664507
},
{
- "top_left_x": 1.057313,
- "top_left_y": 1.057313,
- "top_right_x": 1.057313,
- "top_right_y": 1.057313,
- "bottom_right_x": 2.114626,
- "bottom_right_y": 2.114626,
- "bottom_left_x": 2.114626,
- "bottom_left_y": 2.114626
+ "top_left_x": 1.8252907,
+ "top_left_y": 1.8252907,
+ "top_right_x": 1.8252907,
+ "top_right_y": 1.8252907,
+ "bottom_right_x": 3.6505814,
+ "bottom_right_y": 3.6505814,
+ "bottom_left_x": 3.6505814,
+ "bottom_left_y": 3.6505814
},
{
- "top_left_x": 0.7824335,
- "top_left_y": 0.7824335,
- "top_right_x": 0.7824335,
- "top_right_y": 0.7824335,
- "bottom_right_x": 1.564867,
- "bottom_right_y": 1.564867,
- "bottom_left_x": 1.564867,
- "bottom_left_y": 1.564867
+ "top_left_x": 1.4077549,
+ "top_left_y": 1.4077549,
+ "top_right_x": 1.4077549,
+ "top_right_y": 1.4077549,
+ "bottom_right_x": 2.8155098,
+ "bottom_right_y": 2.8155098,
+ "bottom_left_x": 2.8155098,
+ "bottom_left_y": 2.8155098
},
{
- "top_left_x": 0.5863056,
- "top_left_y": 0.5863056,
- "top_right_x": 0.5863056,
- "top_right_y": 0.5863056,
- "bottom_right_x": 1.1726112,
- "bottom_right_y": 1.1726112,
- "bottom_left_x": 1.1726112,
- "bottom_left_y": 1.1726112
+ "top_left_x": 1.1067667,
+ "top_left_y": 1.1067667,
+ "top_right_x": 1.1067667,
+ "top_right_y": 1.1067667,
+ "bottom_right_x": 2.2135334,
+ "bottom_right_y": 2.2135334,
+ "bottom_left_x": 2.2135334,
+ "bottom_left_y": 2.2135334
},
{
- "top_left_x": 0.4332962,
- "top_left_y": 0.4332962,
- "top_right_x": 0.4332962,
- "top_right_y": 0.4332962,
- "bottom_right_x": 0.8665924,
- "bottom_right_y": 0.8665924,
- "bottom_left_x": 0.8665924,
- "bottom_left_y": 0.8665924
+ "top_left_x": 0.88593864,
+ "top_left_y": 0.88593864,
+ "top_right_x": 0.88593864,
+ "top_right_y": 0.88593864,
+ "bottom_right_x": 1.7718773,
+ "bottom_right_y": 1.7718773,
+ "bottom_left_x": 1.7718773,
+ "bottom_left_y": 1.7718773
},
{
- "top_left_x": 0.3145876,
- "top_left_y": 0.3145876,
- "top_right_x": 0.3145876,
- "top_right_y": 0.3145876,
- "bottom_right_x": 0.6291752,
- "bottom_right_y": 0.6291752,
- "bottom_left_x": 0.6291752,
- "bottom_left_y": 0.6291752
+ "top_left_x": 0.7069988,
+ "top_left_y": 0.7069988,
+ "top_right_x": 0.7069988,
+ "top_right_y": 0.7069988,
+ "bottom_right_x": 1.4139977,
+ "bottom_right_y": 1.4139977,
+ "bottom_left_x": 1.4139977,
+ "bottom_left_y": 1.4139977
},
{
- "top_left_x": 0.22506618,
- "top_left_y": 0.22506618,
- "top_right_x": 0.22506618,
- "top_right_y": 0.22506618,
- "bottom_right_x": 0.45013237,
- "bottom_right_y": 0.45013237,
- "bottom_left_x": 0.45013237,
- "bottom_left_y": 0.45013237
+ "top_left_x": 0.55613136,
+ "top_left_y": 0.55613136,
+ "top_right_x": 0.55613136,
+ "top_right_y": 0.55613136,
+ "bottom_right_x": 1.1122627,
+ "bottom_right_y": 1.1122627,
+ "bottom_left_x": 1.1122627,
+ "bottom_left_y": 1.1122627
},
{
- "top_left_x": 0.15591621,
- "top_left_y": 0.15591621,
- "top_right_x": 0.15591621,
- "top_right_y": 0.15591621,
- "bottom_right_x": 0.31183243,
- "bottom_right_y": 0.31183243,
- "bottom_left_x": 0.31183243,
- "bottom_left_y": 0.31183243
+ "top_left_x": 0.44889355,
+ "top_left_y": 0.44889355,
+ "top_right_x": 0.44889355,
+ "top_right_y": 0.44889355,
+ "bottom_right_x": 0.8977871,
+ "bottom_right_y": 0.8977871,
+ "bottom_left_x": 0.8977871,
+ "bottom_left_y": 0.8977871
},
{
- "top_left_x": 0.100948334,
- "top_left_y": 0.100948334,
- "top_right_x": 0.100948334,
- "top_right_y": 0.100948334,
- "bottom_right_x": 0.20189667,
- "bottom_right_y": 0.20189667,
- "bottom_left_x": 0.20189667,
- "bottom_left_y": 0.20189667
+ "top_left_x": 0.34557533,
+ "top_left_y": 0.34557533,
+ "top_right_x": 0.34557533,
+ "top_right_y": 0.34557533,
+ "bottom_right_x": 0.69115067,
+ "bottom_right_y": 0.69115067,
+ "bottom_left_x": 0.69115067,
+ "bottom_left_y": 0.69115067
},
{
- "top_left_x": 0.06496239,
- "top_left_y": 0.06496239,
- "top_right_x": 0.06496239,
- "top_right_y": 0.06496239,
- "bottom_right_x": 0.12992477,
- "bottom_right_y": 0.12992477,
- "bottom_left_x": 0.12992477,
- "bottom_left_y": 0.12992477
+ "top_left_x": 0.27671337,
+ "top_left_y": 0.27671337,
+ "top_right_x": 0.27671337,
+ "top_right_y": 0.27671337,
+ "bottom_right_x": 0.55342674,
+ "bottom_right_y": 0.55342674,
+ "bottom_left_x": 0.55342674,
+ "bottom_left_y": 0.55342674
},
{
- "top_left_x": 0.03526497,
- "top_left_y": 0.03526497,
- "top_right_x": 0.03526497,
- "top_right_y": 0.03526497,
- "bottom_right_x": 0.07052994,
- "bottom_right_y": 0.07052994,
- "bottom_left_x": 0.07052994,
- "bottom_left_y": 0.07052994
+ "top_left_x": 0.20785141,
+ "top_left_y": 0.20785141,
+ "top_right_x": 0.20785141,
+ "top_right_y": 0.20785141,
+ "bottom_right_x": 0.41570282,
+ "bottom_right_y": 0.41570282,
+ "bottom_left_x": 0.41570282,
+ "bottom_left_y": 0.41570282
},
{
- "top_left_x": 0.014661789,
- "top_left_y": 0.014661789,
- "top_right_x": 0.014661789,
- "top_right_y": 0.014661789,
- "bottom_right_x": 0.029323578,
- "bottom_right_y": 0.029323578,
- "bottom_left_x": 0.029323578,
- "bottom_left_y": 0.029323578
+ "top_left_x": 0.1601448,
+ "top_left_y": 0.1601448,
+ "top_right_x": 0.1601448,
+ "top_right_y": 0.1601448,
+ "bottom_right_x": 0.3202896,
+ "bottom_right_y": 0.3202896,
+ "bottom_left_x": 0.3202896,
+ "bottom_left_y": 0.3202896
},
{
- "top_left_x": 0.0041856766,
- "top_left_y": 0.0041856766,
- "top_right_x": 0.0041856766,
- "top_right_y": 0.0041856766,
- "bottom_right_x": 0.008371353,
- "bottom_right_y": 0.008371353,
- "bottom_left_x": 0.008371353,
- "bottom_left_y": 0.008371353
+ "top_left_x": 0.117860794,
+ "top_left_y": 0.117860794,
+ "top_right_x": 0.117860794,
+ "top_right_y": 0.117860794,
+ "bottom_right_x": 0.23572159,
+ "bottom_right_y": 0.23572159,
+ "bottom_left_x": 0.23572159,
+ "bottom_left_y": 0.23572159
+ },
+ {
+ "top_left_x": 0.08036041,
+ "top_left_y": 0.08036041,
+ "top_right_x": 0.08036041,
+ "top_right_y": 0.08036041,
+ "bottom_right_x": 0.16072083,
+ "bottom_right_y": 0.16072083,
+ "bottom_left_x": 0.16072083,
+ "bottom_left_y": 0.16072083
+ },
+ {
+ "top_left_x": 0.05836296,
+ "top_left_y": 0.05836296,
+ "top_right_x": 0.05836296,
+ "top_right_y": 0.05836296,
+ "bottom_right_x": 0.11672592,
+ "bottom_right_y": 0.11672592,
+ "bottom_left_x": 0.11672592,
+ "bottom_left_y": 0.11672592
+ },
+ {
+ "top_left_x": 0.03636551,
+ "top_left_y": 0.03636551,
+ "top_right_x": 0.03636551,
+ "top_right_y": 0.03636551,
+ "bottom_right_x": 0.07273102,
+ "bottom_right_y": 0.07273102,
+ "bottom_left_x": 0.07273102,
+ "bottom_left_y": 0.07273102
+ },
+ {
+ "top_left_x": 0.018137932,
+ "top_left_y": 0.018137932,
+ "top_right_x": 0.018137932,
+ "top_right_y": 0.018137932,
+ "bottom_right_x": 0.036275864,
+ "bottom_right_y": 0.036275864,
+ "bottom_left_x": 0.036275864,
+ "bottom_left_y": 0.036275864
+ },
+ {
+ "top_left_x": 0.0082063675,
+ "top_left_y": 0.0082063675,
+ "top_right_x": 0.0082063675,
+ "top_right_y": 0.0082063675,
+ "bottom_right_x": 0.016412735,
+ "bottom_right_y": 0.016412735,
+ "bottom_left_x": 0.016412735,
+ "bottom_left_y": 0.016412735
+ },
+ {
+ "top_left_x": 0.0031013489,
+ "top_left_y": 0.0031013489,
+ "top_right_x": 0.0031013489,
+ "top_right_y": 0.0031013489,
+ "bottom_right_x": 0.0062026978,
+ "bottom_right_y": 0.0062026978,
+ "bottom_left_x": 0.0062026978,
+ "bottom_left_y": 0.0062026978
},
{
"top_left_x": 0,
@@ -367,19 +461,24 @@
"type": "int",
"data_points": [
0,
+ 96,
+ 153,
+ 192,
+ 220,
+ 238,
+ 249,
+ 254,
+ 233,
+ 191,
+ 153,
+ 117,
+ 85,
+ 57,
+ 33,
+ 14,
+ 3,
0,
- 115,
- 178,
- 217,
- 241,
- 253,
- 239,
- 183,
- 135,
- 91,
- 53,
- 23,
- 5,
+ 0,
0,
0,
0,
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching_withSpring.json b/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching_withSpring.json
new file mode 100644
index 0000000..a840d3c
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching_withSpring.json
@@ -0,0 +1,375 @@
+{
+ "frame_ids": [
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "type": "rect",
+ "data_points": [
+ {
+ "left": 0,
+ "top": 0,
+ "right": 0,
+ "bottom": 0
+ },
+ {
+ "left": 94,
+ "top": 284,
+ "right": 206,
+ "bottom": 414
+ },
+ {
+ "left": 83,
+ "top": 251,
+ "right": 219,
+ "bottom": 447
+ },
+ {
+ "left": 70,
+ "top": 212,
+ "right": 234,
+ "bottom": 485
+ },
+ {
+ "left": 57,
+ "top": 173,
+ "right": 250,
+ "bottom": 522
+ },
+ {
+ "left": 46,
+ "top": 139,
+ "right": 264,
+ "bottom": 555
+ },
+ {
+ "left": 36,
+ "top": 109,
+ "right": 276,
+ "bottom": 584
+ },
+ {
+ "left": 28,
+ "top": 84,
+ "right": 285,
+ "bottom": 608
+ },
+ {
+ "left": 21,
+ "top": 65,
+ "right": 293,
+ "bottom": 627
+ },
+ {
+ "left": 16,
+ "top": 49,
+ "right": 300,
+ "bottom": 642
+ },
+ {
+ "left": 12,
+ "top": 36,
+ "right": 305,
+ "bottom": 653
+ },
+ {
+ "left": 9,
+ "top": 27,
+ "right": 308,
+ "bottom": 662
+ },
+ {
+ "left": 7,
+ "top": 20,
+ "right": 312,
+ "bottom": 669
+ },
+ {
+ "left": 5,
+ "top": 14,
+ "right": 314,
+ "bottom": 675
+ },
+ {
+ "left": 4,
+ "top": 11,
+ "right": 315,
+ "bottom": 678
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 317,
+ "bottom": 684
+ },
+ {
+ "left": 1,
+ "top": 4,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 318,
+ "bottom": 686
+ },
+ {
+ "left": 0,
+ "top": 2,
+ "right": 319,
+ "bottom": 687
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 9.492916,
+ "top_left_y": 9.492916,
+ "top_right_x": 9.492916,
+ "top_right_y": 9.492916,
+ "bottom_right_x": 18.985832,
+ "bottom_right_y": 18.985832,
+ "bottom_left_x": 18.985832,
+ "bottom_left_y": 18.985832
+ },
+ {
+ "top_left_x": 8.381761,
+ "top_left_y": 8.381761,
+ "top_right_x": 8.381761,
+ "top_right_y": 8.381761,
+ "bottom_right_x": 16.763521,
+ "bottom_right_y": 16.763521,
+ "bottom_left_x": 16.763521,
+ "bottom_left_y": 16.763521
+ },
+ {
+ "top_left_x": 7.07397,
+ "top_left_y": 7.07397,
+ "top_right_x": 7.07397,
+ "top_right_y": 7.07397,
+ "bottom_right_x": 14.14794,
+ "bottom_right_y": 14.14794,
+ "bottom_left_x": 14.14794,
+ "bottom_left_y": 14.14794
+ },
+ {
+ "top_left_x": 5.7880254,
+ "top_left_y": 5.7880254,
+ "top_right_x": 5.7880254,
+ "top_right_y": 5.7880254,
+ "bottom_right_x": 11.576051,
+ "bottom_right_y": 11.576051,
+ "bottom_left_x": 11.576051,
+ "bottom_left_y": 11.576051
+ },
+ {
+ "top_left_x": 4.6295347,
+ "top_left_y": 4.6295347,
+ "top_right_x": 4.6295347,
+ "top_right_y": 4.6295347,
+ "bottom_right_x": 9.259069,
+ "bottom_right_y": 9.259069,
+ "bottom_left_x": 9.259069,
+ "bottom_left_y": 9.259069
+ },
+ {
+ "top_left_x": 3.638935,
+ "top_left_y": 3.638935,
+ "top_right_x": 3.638935,
+ "top_right_y": 3.638935,
+ "bottom_right_x": 7.27787,
+ "bottom_right_y": 7.27787,
+ "bottom_left_x": 7.27787,
+ "bottom_left_y": 7.27787
+ },
+ {
+ "top_left_x": 2.8209057,
+ "top_left_y": 2.8209057,
+ "top_right_x": 2.8209057,
+ "top_right_y": 2.8209057,
+ "bottom_right_x": 5.6418114,
+ "bottom_right_y": 5.6418114,
+ "bottom_left_x": 5.6418114,
+ "bottom_left_y": 5.6418114
+ },
+ {
+ "top_left_x": 2.1620893,
+ "top_left_y": 2.1620893,
+ "top_right_x": 2.1620893,
+ "top_right_y": 2.1620893,
+ "bottom_right_x": 4.3241787,
+ "bottom_right_y": 4.3241787,
+ "bottom_left_x": 4.3241787,
+ "bottom_left_y": 4.3241787
+ },
+ {
+ "top_left_x": 1.6414614,
+ "top_left_y": 1.6414614,
+ "top_right_x": 1.6414614,
+ "top_right_y": 1.6414614,
+ "bottom_right_x": 3.2829227,
+ "bottom_right_y": 3.2829227,
+ "bottom_left_x": 3.2829227,
+ "bottom_left_y": 3.2829227
+ },
+ {
+ "top_left_x": 1.2361269,
+ "top_left_y": 1.2361269,
+ "top_right_x": 1.2361269,
+ "top_right_y": 1.2361269,
+ "bottom_right_x": 2.4722538,
+ "bottom_right_y": 2.4722538,
+ "bottom_left_x": 2.4722538,
+ "bottom_left_y": 2.4722538
+ },
+ {
+ "top_left_x": 0.92435074,
+ "top_left_y": 0.92435074,
+ "top_right_x": 0.92435074,
+ "top_right_y": 0.92435074,
+ "bottom_right_x": 1.8487015,
+ "bottom_right_y": 1.8487015,
+ "bottom_left_x": 1.8487015,
+ "bottom_left_y": 1.8487015
+ },
+ {
+ "top_left_x": 0.68693924,
+ "top_left_y": 0.68693924,
+ "top_right_x": 0.68693924,
+ "top_right_y": 0.68693924,
+ "bottom_right_x": 1.3738785,
+ "bottom_right_y": 1.3738785,
+ "bottom_left_x": 1.3738785,
+ "bottom_left_y": 1.3738785
+ },
+ {
+ "top_left_x": 0.5076904,
+ "top_left_y": 0.5076904,
+ "top_right_x": 0.5076904,
+ "top_right_y": 0.5076904,
+ "bottom_right_x": 1.0153809,
+ "bottom_right_y": 1.0153809,
+ "bottom_left_x": 1.0153809,
+ "bottom_left_y": 1.0153809
+ },
+ {
+ "top_left_x": 0.3733511,
+ "top_left_y": 0.3733511,
+ "top_right_x": 0.3733511,
+ "top_right_y": 0.3733511,
+ "bottom_right_x": 0.7467022,
+ "bottom_right_y": 0.7467022,
+ "bottom_left_x": 0.7467022,
+ "bottom_left_y": 0.7467022
+ },
+ {
+ "top_left_x": 0.27331638,
+ "top_left_y": 0.27331638,
+ "top_right_x": 0.27331638,
+ "top_right_y": 0.27331638,
+ "bottom_right_x": 0.54663277,
+ "bottom_right_y": 0.54663277,
+ "bottom_left_x": 0.54663277,
+ "bottom_left_y": 0.54663277
+ },
+ {
+ "top_left_x": 0.19925308,
+ "top_left_y": 0.19925308,
+ "top_right_x": 0.19925308,
+ "top_right_y": 0.19925308,
+ "bottom_right_x": 0.39850616,
+ "bottom_right_y": 0.39850616,
+ "bottom_left_x": 0.39850616,
+ "bottom_left_y": 0.39850616
+ },
+ {
+ "top_left_x": 0.14470005,
+ "top_left_y": 0.14470005,
+ "top_right_x": 0.14470005,
+ "top_right_y": 0.14470005,
+ "bottom_right_x": 0.2894001,
+ "bottom_right_y": 0.2894001,
+ "bottom_left_x": 0.2894001,
+ "bottom_left_y": 0.2894001
+ },
+ {
+ "top_left_x": 0.10470486,
+ "top_left_y": 0.10470486,
+ "top_right_x": 0.10470486,
+ "top_right_y": 0.10470486,
+ "bottom_right_x": 0.20940971,
+ "bottom_right_y": 0.20940971,
+ "bottom_left_x": 0.20940971,
+ "bottom_left_y": 0.20940971
+ },
+ {
+ "top_left_x": 0.07550812,
+ "top_left_y": 0.07550812,
+ "top_right_x": 0.07550812,
+ "top_right_y": 0.07550812,
+ "bottom_right_x": 0.15101624,
+ "bottom_right_y": 0.15101624,
+ "bottom_left_x": 0.15101624,
+ "bottom_left_y": 0.15101624
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 45,
+ 126,
+ 190,
+ 228,
+ 246,
+ 253,
+ 255,
+ 255,
+ 255,
+ 249,
+ 226,
+ 192,
+ 153,
+ 112,
+ 72,
+ 34,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning.json b/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning.json
index 608e633..aa80445 100644
--- a/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning.json
+++ b/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning.json
@@ -1,25 +1,30 @@
{
"frame_ids": [
- "before",
0,
- 26,
- 52,
- 78,
- 105,
- 131,
- 157,
- 184,
- 210,
- 236,
- 263,
- 289,
- 315,
- 342,
- 368,
- 394,
- 421,
- 447,
- 473,
+ 20,
+ 40,
+ 60,
+ 80,
+ 100,
+ 120,
+ 140,
+ 160,
+ 180,
+ 200,
+ 220,
+ 240,
+ 260,
+ 280,
+ 300,
+ 320,
+ 340,
+ 360,
+ 380,
+ 400,
+ 420,
+ 440,
+ 460,
+ 480,
500
],
"features": [
@@ -28,70 +33,82 @@
"type": "rect",
"data_points": [
{
- "left": 0,
- "top": 0,
- "right": 0,
- "bottom": 0
- },
- {
"left": 100,
"top": 300,
"right": 200,
"bottom": 400
},
{
- "left": 98,
- "top": 293,
- "right": 203,
- "bottom": 407
+ "left": 99,
+ "top": 296,
+ "right": 202,
+ "bottom": 404
},
{
- "left": 91,
- "top": 269,
- "right": 213,
- "bottom": 430
+ "left": 95,
+ "top": 283,
+ "right": 207,
+ "bottom": 417
},
{
- "left": 71,
- "top": 206,
- "right": 240,
- "bottom": 491
+ "left": 86,
+ "top": 256,
+ "right": 219,
+ "bottom": 443
},
{
- "left": 34,
- "top": 98,
- "right": 283,
- "bottom": 595
+ "left": 68,
+ "top": 198,
+ "right": 243,
+ "bottom": 499
},
{
- "left": 22,
- "top": 63,
- "right": 296,
- "bottom": 629
+ "left": 39,
+ "top": 110,
+ "right": 278,
+ "bottom": 584
+ },
+ {
+ "left": 26,
+ "top": 74,
+ "right": 292,
+ "bottom": 618
+ },
+ {
+ "left": 19,
+ "top": 55,
+ "right": 299,
+ "bottom": 637
},
{
"left": 15,
- "top": 44,
- "right": 303,
- "bottom": 648
+ "top": 42,
+ "right": 304,
+ "bottom": 649
},
{
- "left": 11,
- "top": 32,
- "right": 308,
- "bottom": 659
+ "left": 12,
+ "top": 33,
+ "right": 307,
+ "bottom": 658
},
{
- "left": 8,
- "top": 23,
- "right": 311,
- "bottom": 667
+ "left": 9,
+ "top": 27,
+ "right": 310,
+ "bottom": 664
+ },
+ {
+ "left": 7,
+ "top": 21,
+ "right": 312,
+ "bottom": 669
},
{
"left": 6,
- "top": 18,
- "right": 313,
- "bottom": 673
+ "top": 17,
+ "right": 314,
+ "bottom": 674
},
{
"left": 5,
@@ -100,16 +117,22 @@
"bottom": 677
},
{
- "left": 3,
- "top": 9,
+ "left": 4,
+ "top": 10,
"right": 316,
- "bottom": 681
+ "bottom": 680
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 317,
+ "bottom": 682
},
{
"left": 2,
- "top": 7,
- "right": 317,
- "bottom": 683
+ "top": 6,
+ "right": 318,
+ "bottom": 684
},
{
"left": 2,
@@ -119,7 +142,7 @@
},
{
"left": 1,
- "top": 3,
+ "top": 4,
"right": 319,
"bottom": 687
},
@@ -130,6 +153,18 @@
"bottom": 688
},
{
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
"left": 0,
"top": 1,
"right": 320,
@@ -159,7 +194,6 @@
"name": "corner_radii",
"type": "cornerRadii",
"data_points": [
- null,
{
"top_left_x": 10,
"top_left_y": 10,
@@ -171,184 +205,244 @@
"bottom_left_y": 20
},
{
- "top_left_x": 9.762664,
- "top_left_y": 9.762664,
- "top_right_x": 9.762664,
- "top_right_y": 9.762664,
- "bottom_right_x": 19.525328,
- "bottom_right_y": 19.525328,
- "bottom_left_x": 19.525328,
- "bottom_left_y": 19.525328
+ "top_left_x": 9.865689,
+ "top_left_y": 9.865689,
+ "top_right_x": 9.865689,
+ "top_right_y": 9.865689,
+ "bottom_right_x": 19.731379,
+ "bottom_right_y": 19.731379,
+ "bottom_left_x": 19.731379,
+ "bottom_left_y": 19.731379
},
{
- "top_left_x": 8.969244,
- "top_left_y": 8.969244,
- "top_right_x": 8.969244,
- "top_right_y": 8.969244,
- "bottom_right_x": 17.938488,
- "bottom_right_y": 17.938488,
- "bottom_left_x": 17.938488,
- "bottom_left_y": 17.938488
+ "top_left_x": 9.419104,
+ "top_left_y": 9.419104,
+ "top_right_x": 9.419104,
+ "top_right_y": 9.419104,
+ "bottom_right_x": 18.838207,
+ "bottom_right_y": 18.838207,
+ "bottom_left_x": 18.838207,
+ "bottom_left_y": 18.838207
},
{
- "top_left_x": 6.8709626,
- "top_left_y": 6.8709626,
- "top_right_x": 6.8709626,
- "top_right_y": 6.8709626,
- "bottom_right_x": 13.741925,
- "bottom_right_y": 13.741925,
- "bottom_left_x": 13.741925,
- "bottom_left_y": 13.741925
+ "top_left_x": 8.533693,
+ "top_left_y": 8.533693,
+ "top_right_x": 8.533693,
+ "top_right_y": 8.533693,
+ "bottom_right_x": 17.067387,
+ "bottom_right_y": 17.067387,
+ "bottom_left_x": 17.067387,
+ "bottom_left_y": 17.067387
},
{
- "top_left_x": 3.260561,
- "top_left_y": 3.260561,
- "top_right_x": 3.260561,
- "top_right_y": 3.260561,
- "bottom_right_x": 6.521122,
- "bottom_right_y": 6.521122,
- "bottom_left_x": 6.521122,
- "bottom_left_y": 6.521122
+ "top_left_x": 6.5919456,
+ "top_left_y": 6.5919456,
+ "top_right_x": 6.5919456,
+ "top_right_y": 6.5919456,
+ "bottom_right_x": 13.183891,
+ "bottom_right_y": 13.183891,
+ "bottom_left_x": 13.183891,
+ "bottom_left_y": 13.183891
},
{
- "top_left_x": 2.0915751,
- "top_left_y": 2.0915751,
- "top_right_x": 2.0915751,
- "top_right_y": 2.0915751,
- "bottom_right_x": 4.1831503,
- "bottom_right_y": 4.1831503,
- "bottom_left_x": 4.1831503,
- "bottom_left_y": 4.1831503
+ "top_left_x": 3.6674318,
+ "top_left_y": 3.6674318,
+ "top_right_x": 3.6674318,
+ "top_right_y": 3.6674318,
+ "bottom_right_x": 7.3348637,
+ "bottom_right_y": 7.3348637,
+ "bottom_left_x": 7.3348637,
+ "bottom_left_y": 7.3348637
},
{
- "top_left_x": 1.4640827,
- "top_left_y": 1.4640827,
- "top_right_x": 1.4640827,
- "top_right_y": 1.4640827,
- "bottom_right_x": 2.9281654,
- "bottom_right_y": 2.9281654,
- "bottom_left_x": 2.9281654,
- "bottom_left_y": 2.9281654
+ "top_left_x": 2.4832253,
+ "top_left_y": 2.4832253,
+ "top_right_x": 2.4832253,
+ "top_right_y": 2.4832253,
+ "bottom_right_x": 4.9664507,
+ "bottom_right_y": 4.9664507,
+ "bottom_left_x": 4.9664507,
+ "bottom_left_y": 4.9664507
},
{
- "top_left_x": 1.057313,
- "top_left_y": 1.057313,
- "top_right_x": 1.057313,
- "top_right_y": 1.057313,
- "bottom_right_x": 2.114626,
- "bottom_right_y": 2.114626,
- "bottom_left_x": 2.114626,
- "bottom_left_y": 2.114626
+ "top_left_x": 1.8252907,
+ "top_left_y": 1.8252907,
+ "top_right_x": 1.8252907,
+ "top_right_y": 1.8252907,
+ "bottom_right_x": 3.6505814,
+ "bottom_right_y": 3.6505814,
+ "bottom_left_x": 3.6505814,
+ "bottom_left_y": 3.6505814
},
{
- "top_left_x": 0.7824335,
- "top_left_y": 0.7824335,
- "top_right_x": 0.7824335,
- "top_right_y": 0.7824335,
- "bottom_right_x": 1.564867,
- "bottom_right_y": 1.564867,
- "bottom_left_x": 1.564867,
- "bottom_left_y": 1.564867
+ "top_left_x": 1.4077549,
+ "top_left_y": 1.4077549,
+ "top_right_x": 1.4077549,
+ "top_right_y": 1.4077549,
+ "bottom_right_x": 2.8155098,
+ "bottom_right_y": 2.8155098,
+ "bottom_left_x": 2.8155098,
+ "bottom_left_y": 2.8155098
},
{
- "top_left_x": 0.5863056,
- "top_left_y": 0.5863056,
- "top_right_x": 0.5863056,
- "top_right_y": 0.5863056,
- "bottom_right_x": 1.1726112,
- "bottom_right_y": 1.1726112,
- "bottom_left_x": 1.1726112,
- "bottom_left_y": 1.1726112
+ "top_left_x": 1.1067667,
+ "top_left_y": 1.1067667,
+ "top_right_x": 1.1067667,
+ "top_right_y": 1.1067667,
+ "bottom_right_x": 2.2135334,
+ "bottom_right_y": 2.2135334,
+ "bottom_left_x": 2.2135334,
+ "bottom_left_y": 2.2135334
},
{
- "top_left_x": 0.4332962,
- "top_left_y": 0.4332962,
- "top_right_x": 0.4332962,
- "top_right_y": 0.4332962,
- "bottom_right_x": 0.8665924,
- "bottom_right_y": 0.8665924,
- "bottom_left_x": 0.8665924,
- "bottom_left_y": 0.8665924
+ "top_left_x": 0.88593864,
+ "top_left_y": 0.88593864,
+ "top_right_x": 0.88593864,
+ "top_right_y": 0.88593864,
+ "bottom_right_x": 1.7718773,
+ "bottom_right_y": 1.7718773,
+ "bottom_left_x": 1.7718773,
+ "bottom_left_y": 1.7718773
},
{
- "top_left_x": 0.3145876,
- "top_left_y": 0.3145876,
- "top_right_x": 0.3145876,
- "top_right_y": 0.3145876,
- "bottom_right_x": 0.6291752,
- "bottom_right_y": 0.6291752,
- "bottom_left_x": 0.6291752,
- "bottom_left_y": 0.6291752
+ "top_left_x": 0.7069988,
+ "top_left_y": 0.7069988,
+ "top_right_x": 0.7069988,
+ "top_right_y": 0.7069988,
+ "bottom_right_x": 1.4139977,
+ "bottom_right_y": 1.4139977,
+ "bottom_left_x": 1.4139977,
+ "bottom_left_y": 1.4139977
},
{
- "top_left_x": 0.22506618,
- "top_left_y": 0.22506618,
- "top_right_x": 0.22506618,
- "top_right_y": 0.22506618,
- "bottom_right_x": 0.45013237,
- "bottom_right_y": 0.45013237,
- "bottom_left_x": 0.45013237,
- "bottom_left_y": 0.45013237
+ "top_left_x": 0.55613136,
+ "top_left_y": 0.55613136,
+ "top_right_x": 0.55613136,
+ "top_right_y": 0.55613136,
+ "bottom_right_x": 1.1122627,
+ "bottom_right_y": 1.1122627,
+ "bottom_left_x": 1.1122627,
+ "bottom_left_y": 1.1122627
},
{
- "top_left_x": 0.15591621,
- "top_left_y": 0.15591621,
- "top_right_x": 0.15591621,
- "top_right_y": 0.15591621,
- "bottom_right_x": 0.31183243,
- "bottom_right_y": 0.31183243,
- "bottom_left_x": 0.31183243,
- "bottom_left_y": 0.31183243
+ "top_left_x": 0.44889355,
+ "top_left_y": 0.44889355,
+ "top_right_x": 0.44889355,
+ "top_right_y": 0.44889355,
+ "bottom_right_x": 0.8977871,
+ "bottom_right_y": 0.8977871,
+ "bottom_left_x": 0.8977871,
+ "bottom_left_y": 0.8977871
},
{
- "top_left_x": 0.100948334,
- "top_left_y": 0.100948334,
- "top_right_x": 0.100948334,
- "top_right_y": 0.100948334,
- "bottom_right_x": 0.20189667,
- "bottom_right_y": 0.20189667,
- "bottom_left_x": 0.20189667,
- "bottom_left_y": 0.20189667
+ "top_left_x": 0.34557533,
+ "top_left_y": 0.34557533,
+ "top_right_x": 0.34557533,
+ "top_right_y": 0.34557533,
+ "bottom_right_x": 0.69115067,
+ "bottom_right_y": 0.69115067,
+ "bottom_left_x": 0.69115067,
+ "bottom_left_y": 0.69115067
},
{
- "top_left_x": 0.06496239,
- "top_left_y": 0.06496239,
- "top_right_x": 0.06496239,
- "top_right_y": 0.06496239,
- "bottom_right_x": 0.12992477,
- "bottom_right_y": 0.12992477,
- "bottom_left_x": 0.12992477,
- "bottom_left_y": 0.12992477
+ "top_left_x": 0.27671337,
+ "top_left_y": 0.27671337,
+ "top_right_x": 0.27671337,
+ "top_right_y": 0.27671337,
+ "bottom_right_x": 0.55342674,
+ "bottom_right_y": 0.55342674,
+ "bottom_left_x": 0.55342674,
+ "bottom_left_y": 0.55342674
},
{
- "top_left_x": 0.03526497,
- "top_left_y": 0.03526497,
- "top_right_x": 0.03526497,
- "top_right_y": 0.03526497,
- "bottom_right_x": 0.07052994,
- "bottom_right_y": 0.07052994,
- "bottom_left_x": 0.07052994,
- "bottom_left_y": 0.07052994
+ "top_left_x": 0.20785141,
+ "top_left_y": 0.20785141,
+ "top_right_x": 0.20785141,
+ "top_right_y": 0.20785141,
+ "bottom_right_x": 0.41570282,
+ "bottom_right_y": 0.41570282,
+ "bottom_left_x": 0.41570282,
+ "bottom_left_y": 0.41570282
},
{
- "top_left_x": 0.014661789,
- "top_left_y": 0.014661789,
- "top_right_x": 0.014661789,
- "top_right_y": 0.014661789,
- "bottom_right_x": 0.029323578,
- "bottom_right_y": 0.029323578,
- "bottom_left_x": 0.029323578,
- "bottom_left_y": 0.029323578
+ "top_left_x": 0.1601448,
+ "top_left_y": 0.1601448,
+ "top_right_x": 0.1601448,
+ "top_right_y": 0.1601448,
+ "bottom_right_x": 0.3202896,
+ "bottom_right_y": 0.3202896,
+ "bottom_left_x": 0.3202896,
+ "bottom_left_y": 0.3202896
},
{
- "top_left_x": 0.0041856766,
- "top_left_y": 0.0041856766,
- "top_right_x": 0.0041856766,
- "top_right_y": 0.0041856766,
- "bottom_right_x": 0.008371353,
- "bottom_right_y": 0.008371353,
- "bottom_left_x": 0.008371353,
- "bottom_left_y": 0.008371353
+ "top_left_x": 0.117860794,
+ "top_left_y": 0.117860794,
+ "top_right_x": 0.117860794,
+ "top_right_y": 0.117860794,
+ "bottom_right_x": 0.23572159,
+ "bottom_right_y": 0.23572159,
+ "bottom_left_x": 0.23572159,
+ "bottom_left_y": 0.23572159
+ },
+ {
+ "top_left_x": 0.08036041,
+ "top_left_y": 0.08036041,
+ "top_right_x": 0.08036041,
+ "top_right_y": 0.08036041,
+ "bottom_right_x": 0.16072083,
+ "bottom_right_y": 0.16072083,
+ "bottom_left_x": 0.16072083,
+ "bottom_left_y": 0.16072083
+ },
+ {
+ "top_left_x": 0.05836296,
+ "top_left_y": 0.05836296,
+ "top_right_x": 0.05836296,
+ "top_right_y": 0.05836296,
+ "bottom_right_x": 0.11672592,
+ "bottom_right_y": 0.11672592,
+ "bottom_left_x": 0.11672592,
+ "bottom_left_y": 0.11672592
+ },
+ {
+ "top_left_x": 0.03636551,
+ "top_left_y": 0.03636551,
+ "top_right_x": 0.03636551,
+ "top_right_y": 0.03636551,
+ "bottom_right_x": 0.07273102,
+ "bottom_right_y": 0.07273102,
+ "bottom_left_x": 0.07273102,
+ "bottom_left_y": 0.07273102
+ },
+ {
+ "top_left_x": 0.018137932,
+ "top_left_y": 0.018137932,
+ "top_right_x": 0.018137932,
+ "top_right_y": 0.018137932,
+ "bottom_right_x": 0.036275864,
+ "bottom_right_y": 0.036275864,
+ "bottom_left_x": 0.036275864,
+ "bottom_left_y": 0.036275864
+ },
+ {
+ "top_left_x": 0.0082063675,
+ "top_left_y": 0.0082063675,
+ "top_right_x": 0.0082063675,
+ "top_right_y": 0.0082063675,
+ "bottom_right_x": 0.016412735,
+ "bottom_right_y": 0.016412735,
+ "bottom_left_x": 0.016412735,
+ "bottom_left_y": 0.016412735
+ },
+ {
+ "top_left_x": 0.0031013489,
+ "top_left_y": 0.0031013489,
+ "top_right_x": 0.0031013489,
+ "top_right_y": 0.0031013489,
+ "bottom_right_x": 0.0062026978,
+ "bottom_right_y": 0.0062026978,
+ "bottom_left_x": 0.0062026978,
+ "bottom_left_y": 0.0062026978
},
{
"top_left_x": 0,
@@ -367,19 +461,24 @@
"type": "int",
"data_points": [
0,
+ 96,
+ 153,
+ 192,
+ 220,
+ 238,
+ 249,
+ 254,
+ 233,
+ 191,
+ 153,
+ 117,
+ 85,
+ 57,
+ 33,
+ 14,
+ 3,
0,
- 115,
- 178,
- 217,
- 241,
- 253,
- 239,
- 183,
- 135,
- 91,
- 53,
- 23,
- 5,
+ 0,
0,
0,
0,
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning_withSpring.json b/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning_withSpring.json
new file mode 100644
index 0000000..a840d3c
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning_withSpring.json
@@ -0,0 +1,375 @@
+{
+ "frame_ids": [
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "type": "rect",
+ "data_points": [
+ {
+ "left": 0,
+ "top": 0,
+ "right": 0,
+ "bottom": 0
+ },
+ {
+ "left": 94,
+ "top": 284,
+ "right": 206,
+ "bottom": 414
+ },
+ {
+ "left": 83,
+ "top": 251,
+ "right": 219,
+ "bottom": 447
+ },
+ {
+ "left": 70,
+ "top": 212,
+ "right": 234,
+ "bottom": 485
+ },
+ {
+ "left": 57,
+ "top": 173,
+ "right": 250,
+ "bottom": 522
+ },
+ {
+ "left": 46,
+ "top": 139,
+ "right": 264,
+ "bottom": 555
+ },
+ {
+ "left": 36,
+ "top": 109,
+ "right": 276,
+ "bottom": 584
+ },
+ {
+ "left": 28,
+ "top": 84,
+ "right": 285,
+ "bottom": 608
+ },
+ {
+ "left": 21,
+ "top": 65,
+ "right": 293,
+ "bottom": 627
+ },
+ {
+ "left": 16,
+ "top": 49,
+ "right": 300,
+ "bottom": 642
+ },
+ {
+ "left": 12,
+ "top": 36,
+ "right": 305,
+ "bottom": 653
+ },
+ {
+ "left": 9,
+ "top": 27,
+ "right": 308,
+ "bottom": 662
+ },
+ {
+ "left": 7,
+ "top": 20,
+ "right": 312,
+ "bottom": 669
+ },
+ {
+ "left": 5,
+ "top": 14,
+ "right": 314,
+ "bottom": 675
+ },
+ {
+ "left": 4,
+ "top": 11,
+ "right": 315,
+ "bottom": 678
+ },
+ {
+ "left": 3,
+ "top": 8,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 317,
+ "bottom": 684
+ },
+ {
+ "left": 1,
+ "top": 4,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 318,
+ "bottom": 686
+ },
+ {
+ "left": 0,
+ "top": 2,
+ "right": 319,
+ "bottom": 687
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 9.492916,
+ "top_left_y": 9.492916,
+ "top_right_x": 9.492916,
+ "top_right_y": 9.492916,
+ "bottom_right_x": 18.985832,
+ "bottom_right_y": 18.985832,
+ "bottom_left_x": 18.985832,
+ "bottom_left_y": 18.985832
+ },
+ {
+ "top_left_x": 8.381761,
+ "top_left_y": 8.381761,
+ "top_right_x": 8.381761,
+ "top_right_y": 8.381761,
+ "bottom_right_x": 16.763521,
+ "bottom_right_y": 16.763521,
+ "bottom_left_x": 16.763521,
+ "bottom_left_y": 16.763521
+ },
+ {
+ "top_left_x": 7.07397,
+ "top_left_y": 7.07397,
+ "top_right_x": 7.07397,
+ "top_right_y": 7.07397,
+ "bottom_right_x": 14.14794,
+ "bottom_right_y": 14.14794,
+ "bottom_left_x": 14.14794,
+ "bottom_left_y": 14.14794
+ },
+ {
+ "top_left_x": 5.7880254,
+ "top_left_y": 5.7880254,
+ "top_right_x": 5.7880254,
+ "top_right_y": 5.7880254,
+ "bottom_right_x": 11.576051,
+ "bottom_right_y": 11.576051,
+ "bottom_left_x": 11.576051,
+ "bottom_left_y": 11.576051
+ },
+ {
+ "top_left_x": 4.6295347,
+ "top_left_y": 4.6295347,
+ "top_right_x": 4.6295347,
+ "top_right_y": 4.6295347,
+ "bottom_right_x": 9.259069,
+ "bottom_right_y": 9.259069,
+ "bottom_left_x": 9.259069,
+ "bottom_left_y": 9.259069
+ },
+ {
+ "top_left_x": 3.638935,
+ "top_left_y": 3.638935,
+ "top_right_x": 3.638935,
+ "top_right_y": 3.638935,
+ "bottom_right_x": 7.27787,
+ "bottom_right_y": 7.27787,
+ "bottom_left_x": 7.27787,
+ "bottom_left_y": 7.27787
+ },
+ {
+ "top_left_x": 2.8209057,
+ "top_left_y": 2.8209057,
+ "top_right_x": 2.8209057,
+ "top_right_y": 2.8209057,
+ "bottom_right_x": 5.6418114,
+ "bottom_right_y": 5.6418114,
+ "bottom_left_x": 5.6418114,
+ "bottom_left_y": 5.6418114
+ },
+ {
+ "top_left_x": 2.1620893,
+ "top_left_y": 2.1620893,
+ "top_right_x": 2.1620893,
+ "top_right_y": 2.1620893,
+ "bottom_right_x": 4.3241787,
+ "bottom_right_y": 4.3241787,
+ "bottom_left_x": 4.3241787,
+ "bottom_left_y": 4.3241787
+ },
+ {
+ "top_left_x": 1.6414614,
+ "top_left_y": 1.6414614,
+ "top_right_x": 1.6414614,
+ "top_right_y": 1.6414614,
+ "bottom_right_x": 3.2829227,
+ "bottom_right_y": 3.2829227,
+ "bottom_left_x": 3.2829227,
+ "bottom_left_y": 3.2829227
+ },
+ {
+ "top_left_x": 1.2361269,
+ "top_left_y": 1.2361269,
+ "top_right_x": 1.2361269,
+ "top_right_y": 1.2361269,
+ "bottom_right_x": 2.4722538,
+ "bottom_right_y": 2.4722538,
+ "bottom_left_x": 2.4722538,
+ "bottom_left_y": 2.4722538
+ },
+ {
+ "top_left_x": 0.92435074,
+ "top_left_y": 0.92435074,
+ "top_right_x": 0.92435074,
+ "top_right_y": 0.92435074,
+ "bottom_right_x": 1.8487015,
+ "bottom_right_y": 1.8487015,
+ "bottom_left_x": 1.8487015,
+ "bottom_left_y": 1.8487015
+ },
+ {
+ "top_left_x": 0.68693924,
+ "top_left_y": 0.68693924,
+ "top_right_x": 0.68693924,
+ "top_right_y": 0.68693924,
+ "bottom_right_x": 1.3738785,
+ "bottom_right_y": 1.3738785,
+ "bottom_left_x": 1.3738785,
+ "bottom_left_y": 1.3738785
+ },
+ {
+ "top_left_x": 0.5076904,
+ "top_left_y": 0.5076904,
+ "top_right_x": 0.5076904,
+ "top_right_y": 0.5076904,
+ "bottom_right_x": 1.0153809,
+ "bottom_right_y": 1.0153809,
+ "bottom_left_x": 1.0153809,
+ "bottom_left_y": 1.0153809
+ },
+ {
+ "top_left_x": 0.3733511,
+ "top_left_y": 0.3733511,
+ "top_right_x": 0.3733511,
+ "top_right_y": 0.3733511,
+ "bottom_right_x": 0.7467022,
+ "bottom_right_y": 0.7467022,
+ "bottom_left_x": 0.7467022,
+ "bottom_left_y": 0.7467022
+ },
+ {
+ "top_left_x": 0.27331638,
+ "top_left_y": 0.27331638,
+ "top_right_x": 0.27331638,
+ "top_right_y": 0.27331638,
+ "bottom_right_x": 0.54663277,
+ "bottom_right_y": 0.54663277,
+ "bottom_left_x": 0.54663277,
+ "bottom_left_y": 0.54663277
+ },
+ {
+ "top_left_x": 0.19925308,
+ "top_left_y": 0.19925308,
+ "top_right_x": 0.19925308,
+ "top_right_y": 0.19925308,
+ "bottom_right_x": 0.39850616,
+ "bottom_right_y": 0.39850616,
+ "bottom_left_x": 0.39850616,
+ "bottom_left_y": 0.39850616
+ },
+ {
+ "top_left_x": 0.14470005,
+ "top_left_y": 0.14470005,
+ "top_right_x": 0.14470005,
+ "top_right_y": 0.14470005,
+ "bottom_right_x": 0.2894001,
+ "bottom_right_y": 0.2894001,
+ "bottom_left_x": 0.2894001,
+ "bottom_left_y": 0.2894001
+ },
+ {
+ "top_left_x": 0.10470486,
+ "top_left_y": 0.10470486,
+ "top_right_x": 0.10470486,
+ "top_right_y": 0.10470486,
+ "bottom_right_x": 0.20940971,
+ "bottom_right_y": 0.20940971,
+ "bottom_left_x": 0.20940971,
+ "bottom_left_y": 0.20940971
+ },
+ {
+ "top_left_x": 0.07550812,
+ "top_left_y": 0.07550812,
+ "top_right_x": 0.07550812,
+ "top_right_y": 0.07550812,
+ "bottom_right_x": 0.15101624,
+ "bottom_right_y": 0.15101624,
+ "bottom_left_x": 0.15101624,
+ "bottom_left_y": 0.15101624
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 45,
+ 126,
+ 190,
+ 228,
+ 246,
+ 253,
+ 255,
+ 255,
+ 255,
+ 249,
+ 226,
+ 192,
+ 153,
+ 112,
+ 72,
+ 34,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index 762cfa0..8b427fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -16,43 +16,44 @@
package com.android.systemui.animation
-import android.animation.AnimatorSet
+import android.animation.AnimatorRuleRecordingSpec
+import android.animation.AnimatorTestRuleToolkit
+import android.animation.MotionControl
+import android.animation.recordMotion
import android.graphics.drawable.GradientDrawable
import android.platform.test.annotations.MotionTest
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.test.ext.junit.rules.ActivityScenarioRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.EmptyTestActivity
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import kotlin.test.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import platform.test.motion.MotionTestRule
import platform.test.motion.RecordedMotion
-import platform.test.motion.view.AnimationSampling.Companion.evenlySampled
import platform.test.motion.view.DrawableFeatureCaptures
-import platform.test.motion.view.ViewRecordingSpec.Companion.captureWithoutScreenshot
-import platform.test.motion.view.ViewToolkit
-import platform.test.motion.view.record
-import platform.test.screenshot.DeviceEmulationRule
-import platform.test.screenshot.DeviceEmulationSpec
-import platform.test.screenshot.DisplaySpec
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
import platform.test.screenshot.GoldenPathManager
import platform.test.screenshot.PathConfig
@SmallTest
@MotionTest
-@RunWith(AndroidJUnit4::class)
-class TransitionAnimatorTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class TransitionAnimatorTest(val useSpring: Boolean) : SysuiTestCase() {
companion object {
private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens"
- private val emulationSpec =
- DeviceEmulationSpec(DisplaySpec("phone", width = 320, height = 690, densityDpi = 160))
+ @get:Parameters(name = "{0}")
+ @JvmStatic
+ val useSpringValues = booleanArrayOf(false, true).toList()
}
private val kosmos = Kosmos()
@@ -62,31 +63,50 @@
kosmos.fakeExecutor,
ActivityTransitionAnimator.TIMINGS,
ActivityTransitionAnimator.INTERPOLATORS,
+ ActivityTransitionAnimator.SPRING_TIMINGS,
+ ActivityTransitionAnimator.SPRING_INTERPOLATORS,
)
+ private val withSpring =
+ if (useSpring) {
+ "_withSpring"
+ } else {
+ ""
+ }
- @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
@get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
- @get:Rule(order = 2)
- val motionRule = MotionTestRule(ViewToolkit { activityRule.scenario }, pathManager)
+ @get:Rule(order = 2) val animatorTestRule = android.animation.AnimatorTestRule(this)
+ @get:Rule(order = 3)
+ val motionRule =
+ MotionTestRule(AnimatorTestRuleToolkit(animatorTestRule, kosmos.testScope), pathManager)
@Test
fun backgroundAnimation_whenLaunching() {
val backgroundLayer = GradientDrawable().apply { alpha = 0 }
- val animator = setUpTest(backgroundLayer, isLaunching = true)
+ val animator =
+ setUpTest(backgroundLayer, isLaunching = true).apply {
+ getInstrumentation().runOnMainSync { start() }
+ }
val recordedMotion = recordMotion(backgroundLayer, animator)
- motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ motionRule
+ .assertThat(recordedMotion)
+ .timeSeriesMatchesGolden("backgroundAnimation_whenLaunching$withSpring")
}
@Test
fun backgroundAnimation_whenReturning() {
val backgroundLayer = GradientDrawable().apply { alpha = 0 }
- val animator = setUpTest(backgroundLayer, isLaunching = false)
+ val animator =
+ setUpTest(backgroundLayer, isLaunching = false).apply {
+ getInstrumentation().runOnMainSync { start() }
+ }
val recordedMotion = recordMotion(backgroundLayer, animator)
- motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ motionRule
+ .assertThat(recordedMotion)
+ .timeSeriesMatchesGolden("backgroundAnimation_whenReturning$withSpring")
}
@Test
@@ -94,10 +114,13 @@
val backgroundLayer = GradientDrawable().apply { alpha = 0 }
val animator =
setUpTest(backgroundLayer, isLaunching = true, fadeWindowBackgroundLayer = false)
+ .apply { getInstrumentation().runOnMainSync { start() } }
val recordedMotion = recordMotion(backgroundLayer, animator)
- motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ motionRule
+ .assertThat(recordedMotion)
+ .timeSeriesMatchesGolden("backgroundAnimationWithoutFade_whenLaunching$withSpring")
}
@Test
@@ -105,17 +128,20 @@
val backgroundLayer = GradientDrawable().apply { alpha = 0 }
val animator =
setUpTest(backgroundLayer, isLaunching = false, fadeWindowBackgroundLayer = false)
+ .apply { getInstrumentation().runOnMainSync { start() } }
val recordedMotion = recordMotion(backgroundLayer, animator)
- motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ motionRule
+ .assertThat(recordedMotion)
+ .timeSeriesMatchesGolden("backgroundAnimationWithoutFade_whenReturning$withSpring")
}
private fun setUpTest(
backgroundLayer: GradientDrawable,
isLaunching: Boolean,
fadeWindowBackgroundLayer: Boolean = true,
- ): AnimatorSet {
+ ): TransitionAnimator.Animation {
lateinit var transitionContainer: ViewGroup
activityRule.scenario.onActivity { activity ->
transitionContainer = FrameLayout(activity).apply { setBackgroundColor(0x00FF00) }
@@ -124,18 +150,14 @@
waitForIdleSync()
val controller = TestController(transitionContainer, isLaunching)
- val animation =
- transitionAnimator.createAnimation(
- controller,
- controller.createAnimatorState(),
- createEndState(transitionContainer),
- backgroundLayer,
- fadeWindowBackgroundLayer,
- ) as TransitionAnimator.InterpolatedAnimation
- return AnimatorSet().apply {
- duration = animation.animator.duration
- play(animation.animator)
- }
+ return transitionAnimator.createAnimation(
+ controller,
+ controller.createAnimatorState(),
+ createEndState(transitionContainer),
+ backgroundLayer,
+ fadeWindowBackgroundLayer,
+ useSpring,
+ )
}
private fun createEndState(container: ViewGroup): TransitionAnimator.State {
@@ -144,8 +166,8 @@
return TransitionAnimator.State(
left = containerLocation[0],
top = containerLocation[1],
- right = containerLocation[0] + emulationSpec.display.width,
- bottom = containerLocation[1] + emulationSpec.display.height,
+ right = containerLocation[0] + 320,
+ bottom = containerLocation[1] + 690,
topCornerRadius = 0f,
bottomCornerRadius = 0f,
)
@@ -153,16 +175,35 @@
private fun recordMotion(
backgroundLayer: GradientDrawable,
- animator: AnimatorSet,
+ animation: TransitionAnimator.Animation,
): RecordedMotion {
- return motionRule.record(
- animator,
- backgroundLayer.captureWithoutScreenshot(evenlySampled(20)) {
- feature(DrawableFeatureCaptures.bounds, "bounds")
- feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
- feature(DrawableFeatureCaptures.alpha, "alpha")
- },
- )
+ fun record(motionControl: MotionControl, sampleIntervalMs: Long): RecordedMotion {
+ return motionRule.recordMotion(
+ AnimatorRuleRecordingSpec(backgroundLayer, motionControl, sampleIntervalMs) {
+ feature(DrawableFeatureCaptures.bounds, "bounds")
+ feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
+ feature(DrawableFeatureCaptures.alpha, "alpha")
+ }
+ )
+ }
+
+ val motionControl: MotionControl
+ val sampleIntervalMs: Long
+ if (useSpring) {
+ assertTrue { animation is TransitionAnimator.MultiSpringAnimation }
+ motionControl = MotionControl {
+ awaitCondition { (animation as TransitionAnimator.MultiSpringAnimation).isDone }
+ }
+ sampleIntervalMs = 16L
+ } else {
+ assertTrue { animation is TransitionAnimator.InterpolatedAnimation }
+ motionControl = MotionControl { awaitFrames(count = 26) }
+ sampleIntervalMs = 20L
+ }
+
+ var recording: RecordedMotion? = null
+ getInstrumentation().runOnMainSync { recording = record(motionControl, sampleIntervalMs) }
+ return recording!!
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepositoryKosmos.kt
similarity index 68%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepositoryKosmos.kt
index 2f5daaa..0ca025f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSColumnsRepositoryKosmos.kt
@@ -16,6 +16,12 @@
package com.android.systemui.qs.panels.data.repository
+import android.content.res.mainResources
+import com.android.systemui.common.ui.data.repository.configurationRepository
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
-val Kosmos.fixedColumnsRepository by Kosmos.Fixture { FixedColumnsRepository() }
+val Kosmos.qsColumnsRepository by
+ Kosmos.Fixture {
+ QSColumnsRepository(applicationCoroutineScope, mainResources, configurationRepository)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index 546129f..b4317ad 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -18,11 +18,11 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
-import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.qsColumnsViewModel
import com.android.systemui.qs.panels.ui.viewmodel.tileSquishinessViewModel
val Kosmos.infiniteGridLayout by
Kosmos.Fixture {
- InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel, tileSquishinessViewModel)
+ InfiniteGridLayout(iconTilesViewModel, qsColumnsViewModel, tileSquishinessViewModel)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorKosmos.kt
similarity index 78%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorKosmos.kt
index f4d281d..02ed264 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorKosmos.kt
@@ -17,7 +17,6 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.fixedColumnsRepository
+import com.android.systemui.qs.panels.data.repository.qsColumnsRepository
-val Kosmos.fixedColumnsSizeInteractor by
- Kosmos.Fixture { FixedColumnsSizeInteractor(fixedColumnsRepository) }
+val Kosmos.qsColumnsInteractor by Kosmos.Fixture { QSColumnsInteractor(qsColumnsRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
index 85e9265..10d8e1e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
@@ -24,7 +24,7 @@
Kosmos.Fixture {
PaginatedGridViewModel(
iconTilesViewModel,
- fixedColumnsSizeViewModel,
+ qsColumnsViewModel,
iconLabelVisibilityViewModel,
paginatedGridInteractor,
applicationCoroutineScope,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
deleted file mode 100644
index fde174d..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.ui.viewmodel
-
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.partitionedGridViewModel by
- Kosmos.Fixture {
- PartitionedGridViewModel(
- iconTilesViewModel,
- fixedColumnsSizeViewModel,
- iconLabelVisibilityViewModel,
- )
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelKosmos.kt
similarity index 77%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelKosmos.kt
index feadc91..16b2f54 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelKosmos.kt
@@ -17,7 +17,6 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.domain.interactor.fixedColumnsSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.qsColumnsInteractor
-val Kosmos.fixedColumnsSizeViewModel by
- Kosmos.Fixture { FixedColumnsSizeViewModelImpl(fixedColumnsSizeInteractor) }
+val Kosmos.qsColumnsViewModel by Kosmos.Fixture { QSColumnsSizeViewModelImpl(qsColumnsInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
index babbd50..67d9e0e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
@@ -25,7 +25,7 @@
Kosmos.Fixture {
QuickQuickSettingsViewModel(
currentTilesInteractor,
- fixedColumnsSizeViewModel,
+ qsColumnsViewModel,
quickQuickSettingsRowInteractor,
tileSquishinessViewModel,
iconTilesViewModel,
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 3d67ed4..281a2ce 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -909,7 +909,7 @@
break;
case POLICY_TYPE_CLIPBOARD:
if (Flags.crossDeviceClipboard()) {
- if (policyType == DEVICE_POLICY_CUSTOM
+ if (devicePolicy == DEVICE_POLICY_CUSTOM
&& mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index f42f91e..7f1d912 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -441,6 +441,7 @@
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
import com.android.server.contentcapture.ContentCaptureManagerInternal;
+import com.android.server.crashrecovery.CrashRecoveryAdaptor;
import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.criticalevents.CriticalEventLog;
import com.android.server.firewall.IntentFirewall;
@@ -2210,7 +2211,7 @@
mService.mBroadcastController.startBroadcastObservers();
} else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
if (!refactorCrashrecovery()) {
- mService.mPackageWatchdog.onPackagesReady();
+ CrashRecoveryAdaptor.packageWatchdogOnPackagesReady(mService.mPackageWatchdog);
} else {
mService.mCrashRecoveryHelper.registerConnectivityModuleHealthListener();
}
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 221938a..6e09a84 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -90,7 +90,7 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
-import com.android.server.RescueParty;
+import com.android.server.crashrecovery.CrashRecoveryAdaptor;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.sdksandbox.SdkSandboxManagerLocal;
@@ -1382,7 +1382,7 @@
mService.mOomAdjuster.initSettings();
// Now that the settings provider is published we can consider sending in a rescue party.
- RescueParty.onSettingsProviderPublished(mService.mContext);
+ CrashRecoveryAdaptor.rescuePartyOnSettingsProviderPublished(mService.mContext);
}
/**
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 262c76e..31ae966 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1920,175 +1920,8 @@
return false;
}
- boolean needStart = false;
- boolean updateUmState = false;
- UserState uss;
-
- // If the user we are switching to is not currently started, then
- // we need to start it now.
- t.traceBegin("updateStartedUserArrayStarting");
- synchronized (mLock) {
- uss = mStartedUsers.get(userId);
- if (uss == null) {
- uss = new UserState(UserHandle.of(userId));
- uss.mUnlockProgress.addListener(new UserProgressListener());
- mStartedUsers.put(userId, uss);
- updateStartedUserArrayLU();
- needStart = true;
- updateUmState = true;
- } else if (uss.state == UserState.STATE_SHUTDOWN
- || mDoNotAbortShutdownUserIds.contains(userId)) {
- Slogf.i(TAG, "User #" + userId
- + " is shutting down - will start after full shutdown");
- mPendingUserStarts.add(new PendingUserStart(userId, userStartMode,
- unlockListener));
- t.traceEnd(); // updateStartedUserArrayStarting
- return true;
- }
- }
-
- // No matter what, the fact that we're requested to start the user (even if it is
- // already running) puts it towards the end of the mUserLru list.
- addUserToUserLru(userId);
- if (android.multiuser.Flags.scheduleStopOfBackgroundUser()) {
- mHandler.removeEqualMessages(SCHEDULED_STOP_BACKGROUND_USER_MSG,
- Integer.valueOf(userId));
- }
-
- if (unlockListener != null) {
- uss.mUnlockProgress.addListener(unlockListener);
- }
- t.traceEnd(); // updateStartedUserArrayStarting
-
- if (updateUmState) {
- t.traceBegin("setUserState");
- mInjector.getUserManagerInternal().setUserState(userId, uss.state);
- t.traceEnd();
- }
- t.traceBegin("updateConfigurationAndProfileIds");
- if (foreground) {
- // Make sure the old user is no longer considering the display to be on.
- mInjector.reportGlobalUsageEvent(UsageEvents.Event.SCREEN_NON_INTERACTIVE);
- boolean userSwitchUiEnabled;
- synchronized (mLock) {
- mCurrentUserId = userId;
- ActivityManager.invalidateGetCurrentUserIdCache();
- userSwitchUiEnabled = mUserSwitchUiEnabled;
- }
- mInjector.updateUserConfiguration();
- // NOTE: updateProfileRelatedCaches() is called on both if and else parts, ideally
- // it should be moved outside, but for now it's not as there are many calls to
- // external components here afterwards
- updateProfileRelatedCaches();
- dispatchOnBeforeUserSwitching(userId);
- mInjector.getWindowManager().setCurrentUser(userId);
- mInjector.reportCurWakefulnessUsageEvent();
- // Once the internal notion of the active user has switched, we lock the device
- // with the option to show the user switcher on the keyguard.
- if (userSwitchUiEnabled) {
- mInjector.getWindowManager().setSwitchingUser(true);
- // Only lock if the user has a secure keyguard PIN/Pattern/Pwd
- if (mInjector.getKeyguardManager().isDeviceSecure(userId)) {
- // Make sure the device is locked before moving on with the user switch
- mInjector.lockDeviceNowAndWaitForKeyguardShown();
- }
- }
-
- } else {
- updateProfileRelatedCaches();
- // We are starting a non-foreground user. They have already been added to the end
- // of mUserLru, so we need to ensure that the foreground user isn't displaced.
- addUserToUserLru(mCurrentUserId);
- }
- if (userStartMode == USER_START_MODE_BACKGROUND && !userInfo.isProfile()) {
- scheduleStopOfBackgroundUser(userId);
- }
- t.traceEnd();
-
- // Make sure user is in the started state. If it is currently
- // stopping, we need to knock that off.
- if (uss.state == UserState.STATE_STOPPING) {
- t.traceBegin("updateStateStopping");
- // If we are stopping, we haven't sent ACTION_SHUTDOWN,
- // so we can just fairly silently bring the user back from
- // the almost-dead.
- uss.setState(uss.lastState);
- mInjector.getUserManagerInternal().setUserState(userId, uss.state);
- synchronized (mLock) {
- updateStartedUserArrayLU();
- }
- needStart = true;
- t.traceEnd();
- } else if (uss.state == UserState.STATE_SHUTDOWN) {
- t.traceBegin("updateStateShutdown");
- // This means ACTION_SHUTDOWN has been sent, so we will
- // need to treat this as a new boot of the user.
- uss.setState(UserState.STATE_BOOTING);
- mInjector.getUserManagerInternal().setUserState(userId, uss.state);
- synchronized (mLock) {
- updateStartedUserArrayLU();
- }
- needStart = true;
- t.traceEnd();
- }
-
- if (uss.state == UserState.STATE_BOOTING) {
- t.traceBegin("updateStateBooting");
- // Give user manager a chance to propagate user restrictions
- // to other services and prepare app storage
- mInjector.getUserManager().onBeforeStartUser(userId);
-
- // Booting up a new user, need to tell system services about it.
- // Note that this is on the same handler as scheduling of broadcasts,
- // which is important because it needs to go first.
- mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, NO_ARG2));
- t.traceEnd();
- }
-
- t.traceBegin("sendMessages");
- if (foreground) {
- mHandler.sendMessage(mHandler.obtainMessage(USER_CURRENT_MSG, userId, oldUserId));
- mHandler.removeMessages(REPORT_USER_SWITCH_MSG);
- mHandler.removeMessages(USER_SWITCH_TIMEOUT_MSG);
- mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_MSG,
- oldUserId, userId, uss));
- mHandler.sendMessageDelayed(mHandler.obtainMessage(USER_SWITCH_TIMEOUT_MSG,
- oldUserId, userId, uss), getUserSwitchTimeoutMs());
- }
-
- if (userInfo.preCreated) {
- needStart = false;
- }
-
- // In most cases, broadcast for the system user starting/started is sent by
- // ActivityManagerService#systemReady(). However on some HSUM devices (e.g. tablets)
- // the user switches from the system user to a secondary user while running
- // ActivityManagerService#systemReady(), thus broadcast is not sent for the system user.
- // Therefore we send the broadcast for the system user here as well in HSUM.
- // TODO(b/266158156): Improve/refactor the way broadcasts are sent for the system user
- // in HSUM. Ideally it'd be best to have one single place that sends this notification.
- final boolean isSystemUserInHeadlessMode = (userId == UserHandle.USER_SYSTEM)
- && mInjector.isHeadlessSystemUserMode();
- if (needStart || isSystemUserInHeadlessMode) {
- sendUserStartedBroadcast(userId, callingUid, callingPid);
- }
- t.traceEnd();
-
- if (foreground) {
- t.traceBegin("moveUserToForeground");
- moveUserToForeground(uss, userId);
- t.traceEnd();
- } else {
- t.traceBegin("finishUserBoot");
- finishUserBoot(uss);
- t.traceEnd();
- }
-
- if (needStart || isSystemUserInHeadlessMode) {
- t.traceBegin("sendRestartBroadcast");
- sendUserStartingBroadcast(userId, callingUid, callingPid);
- t.traceEnd();
- }
+ mHandler.post(() -> startUserInternalOnHandler(userId, oldUserId, userStartMode,
+ unlockListener, callingUid, callingPid));
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -2096,6 +1929,183 @@
return true;
}
+ private void startUserInternalOnHandler(int userId, int oldUserId, int userStartMode,
+ IProgressListener unlockListener, int callingUid, int callingPid) {
+ final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+ final boolean foreground = userStartMode == USER_START_MODE_FOREGROUND;
+ final UserInfo userInfo = getUserInfo(userId);
+
+ boolean needStart = false;
+ boolean updateUmState = false;
+ UserState uss;
+
+ // If the user we are switching to is not currently started, then
+ // we need to start it now.
+ t.traceBegin("updateStartedUserArrayStarting");
+ synchronized (mLock) {
+ uss = mStartedUsers.get(userId);
+ if (uss == null) {
+ uss = new UserState(UserHandle.of(userId));
+ uss.mUnlockProgress.addListener(new UserProgressListener());
+ mStartedUsers.put(userId, uss);
+ updateStartedUserArrayLU();
+ needStart = true;
+ updateUmState = true;
+ } else if (uss.state == UserState.STATE_SHUTDOWN
+ || mDoNotAbortShutdownUserIds.contains(userId)) {
+ Slogf.i(TAG, "User #" + userId
+ + " is shutting down - will start after full shutdown");
+ mPendingUserStarts.add(new PendingUserStart(userId, userStartMode,
+ unlockListener));
+ t.traceEnd(); // updateStartedUserArrayStarting
+ return;
+ }
+ }
+
+ // No matter what, the fact that we're requested to start the user (even if it is
+ // already running) puts it towards the end of the mUserLru list.
+ addUserToUserLru(userId);
+ if (android.multiuser.Flags.scheduleStopOfBackgroundUser()) {
+ mHandler.removeEqualMessages(SCHEDULED_STOP_BACKGROUND_USER_MSG,
+ Integer.valueOf(userId));
+ }
+
+ if (unlockListener != null) {
+ uss.mUnlockProgress.addListener(unlockListener);
+ }
+ t.traceEnd(); // updateStartedUserArrayStarting
+
+ if (updateUmState) {
+ t.traceBegin("setUserState");
+ mInjector.getUserManagerInternal().setUserState(userId, uss.state);
+ t.traceEnd();
+ }
+ t.traceBegin("updateConfigurationAndProfileIds");
+ if (foreground) {
+ // Make sure the old user is no longer considering the display to be on.
+ mInjector.reportGlobalUsageEvent(UsageEvents.Event.SCREEN_NON_INTERACTIVE);
+ boolean userSwitchUiEnabled;
+ synchronized (mLock) {
+ mCurrentUserId = userId;
+ ActivityManager.invalidateGetCurrentUserIdCache();
+ userSwitchUiEnabled = mUserSwitchUiEnabled;
+ }
+ mInjector.updateUserConfiguration();
+ // NOTE: updateProfileRelatedCaches() is called on both if and else parts, ideally
+ // it should be moved outside, but for now it's not as there are many calls to
+ // external components here afterwards
+ updateProfileRelatedCaches();
+ dispatchOnBeforeUserSwitching(userId);
+ mInjector.getWindowManager().setCurrentUser(userId);
+ mInjector.reportCurWakefulnessUsageEvent();
+ // Once the internal notion of the active user has switched, we lock the device
+ // with the option to show the user switcher on the keyguard.
+ if (userSwitchUiEnabled) {
+ mInjector.getWindowManager().setSwitchingUser(true);
+ // Only lock if the user has a secure keyguard PIN/Pattern/Pwd
+ if (mInjector.getKeyguardManager().isDeviceSecure(userId)) {
+ // Make sure the device is locked before moving on with the user switch
+ mInjector.lockDeviceNowAndWaitForKeyguardShown();
+ }
+ }
+
+ } else {
+ updateProfileRelatedCaches();
+ // We are starting a non-foreground user. They have already been added to the end
+ // of mUserLru, so we need to ensure that the foreground user isn't displaced.
+ addUserToUserLru(mCurrentUserId);
+ }
+ if (userStartMode == USER_START_MODE_BACKGROUND && !userInfo.isProfile()) {
+ scheduleStopOfBackgroundUser(userId);
+ }
+ t.traceEnd();
+
+ // Make sure user is in the started state. If it is currently
+ // stopping, we need to knock that off.
+ if (uss.state == UserState.STATE_STOPPING) {
+ t.traceBegin("updateStateStopping");
+ // If we are stopping, we haven't sent ACTION_SHUTDOWN,
+ // so we can just fairly silently bring the user back from
+ // the almost-dead.
+ uss.setState(uss.lastState);
+ mInjector.getUserManagerInternal().setUserState(userId, uss.state);
+ synchronized (mLock) {
+ updateStartedUserArrayLU();
+ }
+ needStart = true;
+ t.traceEnd();
+ } else if (uss.state == UserState.STATE_SHUTDOWN) {
+ t.traceBegin("updateStateShutdown");
+ // This means ACTION_SHUTDOWN has been sent, so we will
+ // need to treat this as a new boot of the user.
+ uss.setState(UserState.STATE_BOOTING);
+ mInjector.getUserManagerInternal().setUserState(userId, uss.state);
+ synchronized (mLock) {
+ updateStartedUserArrayLU();
+ }
+ needStart = true;
+ t.traceEnd();
+ }
+
+ if (uss.state == UserState.STATE_BOOTING) {
+ t.traceBegin("updateStateBooting");
+ // Give user manager a chance to propagate user restrictions
+ // to other services and prepare app storage
+ mInjector.getUserManager().onBeforeStartUser(userId);
+
+ // Booting up a new user, need to tell system services about it.
+ // Note that this is on the same handler as scheduling of broadcasts,
+ // which is important because it needs to go first.
+ mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, NO_ARG2));
+ t.traceEnd();
+ }
+
+ t.traceBegin("sendMessages");
+ if (foreground) {
+ mHandler.sendMessage(mHandler.obtainMessage(USER_CURRENT_MSG, userId, oldUserId));
+ mHandler.removeMessages(REPORT_USER_SWITCH_MSG);
+ mHandler.removeMessages(USER_SWITCH_TIMEOUT_MSG);
+ mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_MSG,
+ oldUserId, userId, uss));
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(USER_SWITCH_TIMEOUT_MSG,
+ oldUserId, userId, uss), getUserSwitchTimeoutMs());
+ }
+
+ if (userInfo.preCreated) {
+ needStart = false;
+ }
+
+ // In most cases, broadcast for the system user starting/started is sent by
+ // ActivityManagerService#systemReady(). However on some HSUM devices (e.g. tablets)
+ // the user switches from the system user to a secondary user while running
+ // ActivityManagerService#systemReady(), thus broadcast is not sent for the system user.
+ // Therefore we send the broadcast for the system user here as well in HSUM.
+ // TODO(b/266158156): Improve/refactor the way broadcasts are sent for the system user
+ // in HSUM. Ideally it'd be best to have one single place that sends this notification.
+ final boolean isSystemUserInHeadlessMode = (userId == UserHandle.USER_SYSTEM)
+ && mInjector.isHeadlessSystemUserMode();
+ if (needStart || isSystemUserInHeadlessMode) {
+ sendUserStartedBroadcast(userId, callingUid, callingPid);
+ }
+ t.traceEnd();
+
+ if (foreground) {
+ t.traceBegin("moveUserToForeground");
+ moveUserToForeground(uss, userId);
+ t.traceEnd();
+ } else {
+ t.traceBegin("finishUserBoot");
+ finishUserBoot(uss);
+ t.traceEnd();
+ }
+
+ if (needStart || isSystemUserInHeadlessMode) {
+ t.traceBegin("sendRestartBroadcast");
+ sendUserStartingBroadcast(userId, callingUid, callingPid);
+ t.traceEnd();
+ }
+ }
+
/**
* Start user, if it's not already running, and bring it to foreground.
*/
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 35b5171..939aad4 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -187,7 +187,6 @@
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.input.InputManagerInternal;
import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener;
-import com.android.server.inputmethod.InputMethodMenuControllerNew.MenuItem;
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
import com.android.server.pm.UserManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
@@ -4078,65 +4077,6 @@
}
}
- /**
- * Gets the list of Input Method Switcher Menu items and the index of the selected item.
- *
- * @param items the list of input method and subtype items.
- * @param selectedImeId the ID of the selected input method.
- * @param selectedSubtypeIndex the index of the selected subtype in the input method's array of
- * subtypes, or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if no
- * subtype is selected.
- * @param userId the ID of the user for which to get the menu items.
- * @return the list of menu items, and the index of the selected item,
- * or {@code -1} if no item is selected.
- */
- @GuardedBy("ImfLock.class")
- @NonNull
- private Pair<List<MenuItem>, Integer> getInputMethodPickerItems(
- @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId,
- int selectedSubtypeIndex, @UserIdInt int userId) {
- final var bindingController = getInputMethodBindingController(userId);
- final var settings = InputMethodSettingsRepository.get(userId);
-
- if (selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX) {
- // TODO(b/351124299): Check if this fallback logic is still necessary.
- final var curSubtype = bindingController.getCurrentInputMethodSubtype();
- if (curSubtype != null) {
- final var curMethodId = bindingController.getSelectedMethodId();
- final var curImi = settings.getMethodMap().get(curMethodId);
- selectedSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(
- curImi, curSubtype.hashCode());
- }
- }
-
- // No item is selected by default. When we have a list of explicitly enabled
- // subtypes, the implicit subtype is no longer listed. If the implicit one
- // is still selected, no items will be shown as selected.
- int selectedIndex = -1;
- String prevImeId = null;
- final var menuItems = new ArrayList<MenuItem>();
- for (int i = 0; i < items.size(); i++) {
- final var item = items.get(i);
- final var imeId = item.mImi.getId();
- if (imeId.equals(selectedImeId)) {
- final int subtypeIndex = item.mSubtypeIndex;
- // Check if this is the selected IME-subtype pair.
- if ((subtypeIndex == 0 && selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX)
- || subtypeIndex == NOT_A_SUBTYPE_INDEX
- || subtypeIndex == selectedSubtypeIndex) {
- selectedIndex = i;
- }
- }
- final boolean hasHeader = !imeId.equals(prevImeId);
- final boolean hasDivider = hasHeader && prevImeId != null;
- prevImeId = imeId;
- menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi,
- item.mSubtypeIndex, hasHeader, hasDivider));
- }
-
- return new Pair<>(menuItems, selectedIndex);
- }
-
@IInputMethodManagerImpl.PermissionVerified(allOf = {
Manifest.permission.INTERACT_ACROSS_USERS_FULL,
Manifest.permission.WRITE_SECURE_SETTINGS})
@@ -4973,18 +4913,21 @@
+ " preferredInputMethodSubtypeIndex=" + lastInputMethodSubtypeIndex);
}
- final var itemsAndIndex = getInputMethodPickerItems(imList,
- lastInputMethodId, lastInputMethodSubtypeIndex, userId);
- final var menuItems = itemsAndIndex.first;
- final int selectedIndex = itemsAndIndex.second;
-
- if (selectedIndex == -1) {
- Slog.w(TAG, "Switching menu shown with no item selected"
- + ", IME id: " + lastInputMethodId
- + ", subtype index: " + lastInputMethodSubtypeIndex);
+ int selectedSubtypeIndex = lastInputMethodSubtypeIndex;
+ if (selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX) {
+ // TODO(b/351124299): Check if this fallback logic is still necessary.
+ final var bindingController = getInputMethodBindingController(userId);
+ final var curSubtype = bindingController.getCurrentInputMethodSubtype();
+ if (curSubtype != null) {
+ final var curMethodId = bindingController.getSelectedMethodId();
+ final var curImi = settings.getMethodMap().get(curMethodId);
+ selectedSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(
+ curImi, curSubtype.hashCode());
+ }
}
- mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
+ mMenuControllerNew.show(imList, lastInputMethodId, selectedSubtypeIndex, displayId,
+ userId);
} else {
mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
lastInputMethodId, lastInputMethodSubtypeIndex, imList, userId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
index cf2cdc1..1d0e3c6 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -30,7 +30,6 @@
import android.annotation.UserIdInt;
import android.app.AlertDialog;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
import android.os.UserHandle;
import android.provider.Settings;
@@ -48,8 +47,11 @@
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.RecyclerView;
+import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -80,18 +82,27 @@
/**
* Shows the Input Method Switcher Menu, with a list of IMEs and their subtypes.
*
- * @param items the list of menu items.
- * @param selectedIndex the index of the menu item that is selected.
- * If no other IMEs are enabled, this index will be out of reach.
- * @param displayId the ID of the display where the menu was requested.
- * @param userId the ID of the user that requested the menu.
+ * @param items the list of input method and subtype items.
+ * @param selectedImeId the ID of the selected input method.
+ * @param selectedSubtypeIndex the index of the selected subtype in the input method's array of
+ * subtypes, or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if no
+ * subtype is selected.
+ * @param displayId the ID of the display where the menu was requested.
+ * @param userId the ID of the user that requested the menu.
*/
@RequiresPermission(allOf = {INTERACT_ACROSS_USERS, HIDE_OVERLAY_WINDOWS})
- void show(@NonNull List<MenuItem> items, int selectedIndex, int displayId,
- @UserIdInt int userId) {
+ void show(@NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId,
+ int selectedSubtypeIndex, int displayId, @UserIdInt int userId) {
// Hide the menu in case it was already showing.
hide(displayId, userId);
+ final var menuItems = getMenuItems(items);
+ final int selectedIndex = getSelectedIndex(menuItems, selectedImeId, selectedSubtypeIndex);
+ if (selectedIndex == -1) {
+ Slog.w(TAG, "Switching menu shown with no item selected, IME id: " + selectedImeId
+ + ", subtype index: " + selectedSubtypeIndex);
+ }
+
final Context dialogWindowContext = mDialogWindowContext.get(displayId);
final var builder = new AlertDialog.Builder(dialogWindowContext,
com.android.internal.R.style.Theme_DeviceDefault_InputMethodSwitcherDialog);
@@ -104,52 +115,28 @@
dialogWindowContext.getText(com.android.internal.R.string.select_input_method));
builder.setView(contentView);
- final DialogInterface.OnClickListener onClickListener = (dialog, which) -> {
- if (which != selectedIndex) {
- final var item = items.get(which);
+ final OnClickListener onClickListener = (item, isSelected) -> {
+ if (!isSelected) {
InputMethodManagerInternal.get()
.switchToInputMethod(item.mImi.getId(), item.mSubtypeIndex, userId);
}
hide(displayId, userId);
};
- final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null;
- final var languageSettingsIntent = selectedImi != null
- ? selectedImi.createImeLanguageSettingsActivityIntent() : null;
- final boolean isDeviceProvisioned = Settings.Global.getInt(
- dialogWindowContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED,
- 0) != 0;
- final boolean hasLanguageSettingsButton = languageSettingsIntent != null
- && isDeviceProvisioned;
- if (hasLanguageSettingsButton) {
- final View buttonBar = contentView
- .requireViewById(com.android.internal.R.id.button_bar);
- buttonBar.setVisibility(View.VISIBLE);
-
- languageSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- final Button languageSettingsButton = contentView
- .requireViewById(com.android.internal.R.id.button1);
- languageSettingsButton.setVisibility(View.VISIBLE);
- languageSettingsButton.setOnClickListener(v -> {
- v.getContext().startActivityAsUser(languageSettingsIntent, UserHandle.of(userId));
- hide(displayId, userId);
- });
- }
-
// Create the current IME subtypes list.
final RecyclerView recyclerView = contentView
.requireViewById(com.android.internal.R.id.list);
- recyclerView.setAdapter(new Adapter(items, selectedIndex, inflater, onClickListener));
+ recyclerView.setAdapter(new Adapter(menuItems, selectedIndex, inflater, onClickListener));
// Scroll to the currently selected IME. This must run after the recycler view is laid out.
recyclerView.post(() -> recyclerView.scrollToPosition(selectedIndex));
- // Indicate that the list can be scrolled.
- recyclerView.setScrollIndicators(
- hasLanguageSettingsButton ? View.SCROLL_INDICATOR_BOTTOM : 0);
// Request focus to enable rotary scrolling on watches.
recyclerView.requestFocus();
+ final var selectedItem = selectedIndex > -1 ? menuItems.get(selectedIndex) : null;
+ updateLanguageSettingsButton(selectedItem, contentView, displayId, userId);
+
builder.setOnCancelListener(dialog -> hide(displayId, userId));
- mMenuItems = items;
+ mMenuItems = menuItems;
mDialog = builder.create();
mDialog.setCanceledOnTouchOutside(true);
final Window w = mDialog.getWindow();
@@ -208,98 +195,303 @@
}
/**
- * Item to be shown in the Input Method Switcher Menu, containing an input method and
- * optionally an input method subtype.
+ * Creates the list of menu items from the given list of input methods and subtypes. This
+ * handles adding headers and dividers between groups of items from different input methods
+ * as follows:
+ *
+ * <li>If there is only one group, no divider or header will be added.</li>
+ * <li>A divider is added before each group, except the first one.</li>
+ * <li>A header is added before each group (after the divider, if it exists) if the group has
+ * at least two items, or a single item with a subtype name.</li>
+ *
+ * @param items the list of input method and subtype items.
*/
- static class MenuItem {
+ @VisibleForTesting
+ @NonNull
+ static List<MenuItem> getMenuItems(@NonNull List<ImeSubtypeListItem> items) {
+ final var menuItems = new ArrayList<MenuItem>();
+ if (items.isEmpty()) {
+ return menuItems;
+ }
+
+ final var itemsArray = (ArrayList<ImeSubtypeListItem>) items;
+ final int numItems = itemsArray.size();
+ // Initialize to the last IME id to avoid headers if there is only a single IME.
+ String prevImeId = itemsArray.getLast().mImi.getId();
+ boolean firstGroup = true;
+ for (int i = 0; i < numItems; i++) {
+ final var item = itemsArray.get(i);
+
+ final var imeId = item.mImi.getId();
+ final boolean groupChange = !imeId.equals(prevImeId);
+ if (groupChange) {
+ if (!firstGroup) {
+ menuItems.add(DividerItem.getInstance());
+ }
+ // Add a header if we have at least two items, or a single item with a subtype name.
+ final var nextItemId = i + 1 < numItems ? itemsArray.get(i + 1).mImi.getId() : null;
+ final boolean addHeader = item.mSubtypeName != null || imeId.equals(nextItemId);
+ if (addHeader) {
+ menuItems.add(new HeaderItem(item.mImeName));
+ }
+ firstGroup = false;
+ prevImeId = imeId;
+ }
+
+ menuItems.add(new SubtypeItem(item.mImeName, item.mSubtypeName, item.mImi,
+ item.mSubtypeIndex));
+ }
+
+ return menuItems;
+ }
+
+ /**
+ * Gets the index of the selected item.
+ *
+ * @param items the list of menu items.
+ * @param selectedImeId the ID of the selected input method.
+ * @param selectedSubtypeIndex the index of the selected subtype in the input method's array of
+ * subtypes, or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if no
+ * subtype is selected.
+ * @return the index of the selected item, or {@code -1} if no item is selected.
+ */
+ @VisibleForTesting
+ @IntRange(from = -1)
+ static int getSelectedIndex(@NonNull List<MenuItem> items, @Nullable String selectedImeId,
+ int selectedSubtypeIndex) {
+ for (int i = 0; i < items.size(); i++) {
+ final var item = items.get(i);
+ if (item instanceof SubtypeItem subtypeItem) {
+ final var imeId = subtypeItem.mImi.getId();
+ final int subtypeIndex = subtypeItem.mSubtypeIndex;
+ if (imeId.equals(selectedImeId)
+ && ((subtypeIndex == 0 && selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX)
+ || subtypeIndex == NOT_A_SUBTYPE_INDEX
+ || subtypeIndex == selectedSubtypeIndex)) {
+ return i;
+ }
+ }
+ }
+ // Either there is no selected IME, or the selected subtype is enabled but not in the list.
+ // This can happen if an implicit subtype is selected, but we got a list of explicit
+ // subtypes. In this case, the implicit subtype will no longer be included in the list.
+ return -1;
+ }
+
+ /**
+ * Updates the visibility of the Language Settings button to visible if the currently selected
+ * item specifies a (language) settings activity and the device is provisioned. Otherwise,
+ * the button won't be shown.
+ *
+ * @param selectedItem the currently selected item, or {@code null} if no item is selected.
+ * @param view the menu dialog view.
+ * @param displayId the ID of the display where the menu was requested.
+ * @param userId the ID of the user that requested the menu.
+ */
+ @RequiresPermission(allOf = {INTERACT_ACROSS_USERS})
+ private void updateLanguageSettingsButton(@Nullable MenuItem selectedItem, @NonNull View view,
+ int displayId, @UserIdInt int userId) {
+ final var settingsIntent = (selectedItem instanceof SubtypeItem selectedSubtypeItem)
+ ? selectedSubtypeItem.mImi.createImeLanguageSettingsActivityIntent() : null;
+ final boolean isDeviceProvisioned = Settings.Global.getInt(
+ view.getContext().getContentResolver(), Settings.Global.DEVICE_PROVISIONED,
+ 0) != 0;
+ final boolean hasButton = settingsIntent != null && isDeviceProvisioned;
+ final View buttonBar = view.requireViewById(com.android.internal.R.id.button_bar);
+ final Button button = view.requireViewById(com.android.internal.R.id.button1);
+ final RecyclerView recyclerView = view.requireViewById(com.android.internal.R.id.list);
+ if (hasButton) {
+ settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ buttonBar.setVisibility(View.VISIBLE);
+ button.setOnClickListener(v -> {
+ v.getContext().startActivityAsUser(settingsIntent, UserHandle.of(userId));
+ hide(displayId, userId);
+ });
+ // Indicate that the list can be scrolled.
+ recyclerView.setScrollIndicators(View.SCROLL_INDICATOR_BOTTOM);
+ } else {
+ buttonBar.setVisibility(View.GONE);
+ button.setOnClickListener(null);
+ // Remove scroll indicator as there is nothing drawn below the list.
+ recyclerView.setScrollIndicators(0 /* indicators */);
+ }
+ }
+
+ /**
+ * Interface definition for callbacks to be invoked when a {@link SubtypeItem} is clicked.
+ */
+ private interface OnClickListener {
+
+ /**
+ * Called when an item is clicked.
+ *
+ * @param item The item that was clicked.
+ * @param isSelected Whether the item is the currently selected one.
+ */
+ void onClick(@NonNull SubtypeItem item, boolean isSelected);
+ }
+
+ /** Item to be displayed in the menu. */
+ sealed interface MenuItem {}
+
+ /** Subtype item containing an input method and optionally an input method subtype. */
+ static final class SubtypeItem implements MenuItem {
/** The name of the input method. */
@NonNull
- private final CharSequence mImeName;
+ final CharSequence mImeName;
/**
* The name of the input method subtype, or {@code null} if this item doesn't have a
* subtype.
*/
@Nullable
- private final CharSequence mSubtypeName;
+ final CharSequence mSubtypeName;
/** The info of the input method. */
@NonNull
- private final InputMethodInfo mImi;
+ final InputMethodInfo mImi;
/**
* The index of the subtype in the input method's array of subtypes,
* or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if this item doesn't have a subtype.
*/
@IntRange(from = NOT_A_SUBTYPE_INDEX)
- private final int mSubtypeIndex;
+ final int mSubtypeIndex;
- /** Whether this item has a group header (only the first item of each input method). */
- private final boolean mHasHeader;
-
- /**
- * Whether this item should has a group divider (same as {@link #mHasHeader},
- * excluding the first IME).
- */
- private final boolean mHasDivider;
-
- MenuItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName,
+ SubtypeItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName,
@NonNull InputMethodInfo imi,
- @IntRange(from = NOT_A_SUBTYPE_INDEX) int subtypeIndex, boolean hasHeader,
- boolean hasDivider) {
+ @IntRange(from = NOT_A_SUBTYPE_INDEX) int subtypeIndex) {
mImeName = imeName;
mSubtypeName = subtypeName;
mImi = imi;
mSubtypeIndex = subtypeIndex;
- mHasHeader = hasHeader;
- mHasDivider = hasDivider;
}
@Override
public String toString() {
- return "MenuItem{"
+ return "SubtypeItem{"
+ "mImeName=" + mImeName
+ " mSubtypeName=" + mSubtypeName
+ " mSubtypeIndex=" + mSubtypeIndex
- + " mHasHeader=" + mHasHeader
- + " mHasDivider=" + mHasDivider
+ "}";
}
}
- private static class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
+ /** Header item displayed before a group of {@link SubtypeItem} of the same input method. */
+ static final class HeaderItem implements MenuItem {
+
+ /** The header title. */
+ @NonNull
+ final CharSequence mTitle;
+
+ HeaderItem(@NonNull CharSequence title) {
+ mTitle = title;
+ }
+
+ @Override
+ public String toString() {
+ return "HeaderItem{"
+ + "mTitle=" + mTitle
+ + "}";
+ }
+ }
+
+ /** Divider item displayed before a {@link HeaderItem}. */
+ static final class DividerItem implements MenuItem {
+
+ private static DividerItem sInstance;
+
+ /** Gets a singleton instance of DividerItem. */
+ @NonNull
+ static DividerItem getInstance() {
+ if (sInstance == null) {
+ sInstance = new DividerItem();
+ }
+ return sInstance;
+ }
+
+ @Override
+ public String toString() {
+ return "DividerItem{}";
+ }
+ }
+
+ private static final class Adapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ /** View type for unknown item. */
+ private static final int TYPE_UNKNOWN = -1;
+
+ /** View type for {@link SubtypeItem}. */
+ private static final int TYPE_SUBTYPE = 0;
+
+ /** View type for {@link HeaderItem}. */
+ private static final int TYPE_HEADER = 1;
+
+ /** View type for {@link DividerItem}. */
+ private static final int TYPE_DIVIDER = 2;
/** The list of items to show. */
@NonNull
private final List<MenuItem> mItems;
/** The index of the selected item. */
+ @IntRange(from = -1)
private final int mSelectedIndex;
@NonNull
private final LayoutInflater mInflater;
+ /** The listener used to handle clicks on {@link SubtypeViewHolder} items. */
@NonNull
- private final DialogInterface.OnClickListener mOnClickListener;
+ private final OnClickListener mListener;
- Adapter(@NonNull List<MenuItem> items, int selectedIndex,
+ Adapter(@NonNull List<MenuItem> items, @IntRange(from = -1) int selectedIndex,
@NonNull LayoutInflater inflater,
- @NonNull DialogInterface.OnClickListener onClickListener) {
+ @NonNull OnClickListener listener) {
mItems = items;
mSelectedIndex = selectedIndex;
mInflater = inflater;
- mOnClickListener = onClickListener;
+ mListener = listener;
}
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- final View view = mInflater.inflate(
- com.android.internal.R.layout.input_method_switch_item_new, parent, false);
-
- return new ViewHolder(view, mOnClickListener);
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case TYPE_SUBTYPE -> {
+ final View view = mInflater.inflate(
+ com.android.internal.R.layout.input_method_switch_item_new, parent,
+ false);
+ return new SubtypeViewHolder(view, mListener);
+ }
+ case TYPE_HEADER -> {
+ final View view = mInflater.inflate(
+ com.android.internal.R.layout.input_method_switch_item_header, parent,
+ false);
+ return new HeaderViewHolder(view);
+ }
+ case TYPE_DIVIDER -> {
+ final View view = mInflater.inflate(
+ com.android.internal.R.layout.input_method_switch_item_divider, parent,
+ false);
+ return new DividerViewHolder(view);
+ }
+ default -> throw new IllegalArgumentException("Unknown viewType: " + viewType);
+ }
}
@Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- holder.bind(mItems.get(position), position == mSelectedIndex /* isSelected */);
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ final var item = mItems.get(position);
+ if (holder instanceof SubtypeViewHolder subtypeHolder
+ && item instanceof SubtypeItem subtypeItem) {
+ subtypeHolder.bind(subtypeItem, position == mSelectedIndex /* isSelected */);
+ } else if (holder instanceof HeaderViewHolder headerHolder
+ && item instanceof HeaderItem headerItem) {
+ headerHolder.bind(headerItem);
+ } else if (holder instanceof DividerViewHolder && item instanceof DividerItem) {
+ // Nothing to bind for dividers.
+ return;
+ } else {
+ Slog.w(TAG, "Holder type: " + holder + " doesn't match item type: " + item);
+ }
}
@Override
@@ -307,7 +499,21 @@
return mItems.size();
}
- private static class ViewHolder extends RecyclerView.ViewHolder {
+ @Override
+ public int getItemViewType(int position) {
+ final var item = mItems.get(position);
+ if (item instanceof SubtypeItem) {
+ return TYPE_SUBTYPE;
+ } else if (item instanceof HeaderItem) {
+ return TYPE_HEADER;
+ } else if (item instanceof DividerItem) {
+ return TYPE_DIVIDER;
+ } else {
+ return TYPE_UNKNOWN;
+ }
+ }
+
+ private static final class SubtypeViewHolder extends RecyclerView.ViewHolder {
/** The container of the item. */
@NonNull
@@ -318,46 +524,74 @@
/** Indicator for the selected status of the item. */
@NonNull
private final ImageView mCheckmark;
- /** The group header optionally drawn above the item. */
- @NonNull
- private final TextView mHeader;
- /** The group divider optionally drawn above the item. */
- @NonNull
- private final View mDivider;
- private ViewHolder(@NonNull View itemView,
- @NonNull DialogInterface.OnClickListener onClickListener) {
+ /** The bound item data, or {@code null} if no item was bound yet. */
+ @Nullable
+ private SubtypeItem mItem;
+ /** Whether this item is the currently selected one. */
+ private boolean mIsSelected;
+
+ SubtypeViewHolder(@NonNull View itemView, @NonNull OnClickListener listener) {
super(itemView);
- mContainer = itemView.requireViewById(com.android.internal.R.id.list_item);
+ mContainer = itemView;
mName = itemView.requireViewById(com.android.internal.R.id.text);
mCheckmark = itemView.requireViewById(com.android.internal.R.id.image);
- mHeader = itemView.requireViewById(com.android.internal.R.id.header_text);
- mDivider = itemView.requireViewById(com.android.internal.R.id.divider);
- mContainer.setOnClickListener((v) ->
- onClickListener.onClick(null /* dialog */, getAdapterPosition()));
+ mContainer.setOnClickListener((v) -> {
+ if (mItem != null) {
+ listener.onClick(mItem, mIsSelected);
+ }
+ });
}
/**
* Binds the given item to the current view.
*
* @param item the item to bind.
- * @param isSelected whether this is selected.
+ * @param isSelected whether the item is selected.
*/
- private void bind(@NonNull MenuItem item, boolean isSelected) {
+ void bind(@NonNull SubtypeItem item, boolean isSelected) {
+ mItem = item;
+ mIsSelected = isSelected;
// Use the IME name for subtypes with an empty subtype name.
final var name = TextUtils.isEmpty(item.mSubtypeName)
? item.mImeName : item.mSubtypeName;
mContainer.setActivated(isSelected);
// Activated is the correct state, but we also set selected for accessibility info.
mContainer.setSelected(isSelected);
+ // Trigger the ellipsize marquee behaviour by selecting the name.
mName.setSelected(isSelected);
mName.setText(name);
mCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE);
- mHeader.setText(item.mImeName);
- mHeader.setVisibility(item.mHasHeader ? View.VISIBLE : View.GONE);
- mDivider.setVisibility(item.mHasDivider ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private static final class HeaderViewHolder extends RecyclerView.ViewHolder {
+
+ /** The title view, only visible if the bound item has a title. */
+ private final TextView mTitle;
+
+ HeaderViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ mTitle = itemView.requireViewById(com.android.internal.R.id.header_text);
+ }
+
+ /**
+ * Binds the given item to the current view.
+ *
+ * @param item the item to bind.
+ */
+ void bind(@NonNull HeaderItem item) {
+ mTitle.setText(item.mTitle);
+ }
+ }
+
+ private static final class DividerViewHolder extends RecyclerView.ViewHolder {
+
+ DividerViewHolder(@NonNull View itemView) {
+ super(itemView);
}
}
}
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 9e70f81..3349b13 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -1986,6 +1986,7 @@
* bypassing DND. It should be called whenever a channel is created, updated, or deleted, or
* when the current user (or its profiles) change.
*/
+ // TODO: b/368247671 - remove fromSystemOrSystemUi argument when modes_ui is inlined.
private void updateCurrentUserHasChannelsBypassingDnd(int callingUid,
boolean fromSystemOrSystemUi) {
ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
@@ -2016,7 +2017,12 @@
boolean haveBypassingApps = candidatePkgs.size() > 0;
if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) {
mCurrentUserHasChannelsBypassingDnd = haveBypassingApps;
- updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
+ if (android.app.Flags.modesUi()) {
+ mZenModeHelper.updateHasPriorityChannels(mCurrentUserHasChannelsBypassingDnd);
+ } else {
+ updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid,
+ fromSystemOrSystemUi);
+ }
}
}
@@ -2034,6 +2040,9 @@
return true;
}
+ // TODO: b/368247671 - delete this method when modes_ui is inlined, as
+ // updateCurrentUserHasChannelsBypassingDnd was the only caller and
+ // PreferencesHelper should otherwise not need to modify actual policy
public void updateZenPolicy(boolean areChannelsBypassingDnd, int callingUid,
boolean fromSystemOrSystemUi) {
NotificationManager.Policy policy = mZenModeHelper.getNotificationPolicy();
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index ea211a9..5547bd3 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -1567,6 +1567,28 @@
return azr;
}
+ // Update only the hasPriorityChannels state (aka areChannelsBypassingDnd) without modifying
+ // any of the rest of the existing policy. This allows components that only want to modify
+ // this bit (PreferencesHelper) to not have to adjust the rest of the policy.
+ protected void updateHasPriorityChannels(boolean hasPriorityChannels) {
+ if (!Flags.modesUi()) {
+ Log.wtf(TAG, "updateHasPriorityChannels called without modes_ui");
+ }
+ synchronized (mConfigLock) {
+ // If it already matches, do nothing
+ if (mConfig.areChannelsBypassingDnd == hasPriorityChannels) {
+ return;
+ }
+
+ ZenModeConfig newConfig = mConfig.copy();
+ newConfig.areChannelsBypassingDnd = hasPriorityChannels;
+ // The updated calculation of whether there are priority channels is always done by
+ // the system, even if the event causing the calculation had a different origin.
+ setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "updateHasPriorityChannels",
+ Process.SYSTEM_UID);
+ }
+ }
+
@SuppressLint("MissingPermission")
void scheduleActivationBroadcast(String pkg, @UserIdInt int userId, String ruleId,
boolean activated) {
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 0b34177..a24c743 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -180,3 +180,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "notification_vibration_in_sound_uri_for_channel"
+ namespace: "systemui"
+ description: "Enables sound uri with vibration source in notification channel"
+ bug: "351975435"
+}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index b228bb9..be7631d 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2768,7 +2768,8 @@
enforceCrossUserPermission(Binder.getCallingUid(), userId, false, false,
!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId),
"MATCH_ANY_USER flag requires INTERACT_ACROSS_USERS permission");
- } else if ((flags & PackageManager.MATCH_UNINSTALLED_PACKAGES) != 0
+ } else if (!Flags.removeCrossUserPermissionHack()
+ && (flags & PackageManager.MATCH_UNINSTALLED_PACKAGES) != 0
&& isCallerSystemUser
&& mUserManager.hasProfile(UserHandle.USER_SYSTEM)) {
// If the caller wants all packages and has a profile associated with it,
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c8cf938..39f0380 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -204,7 +204,6 @@
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
import com.android.server.LockGuard;
-import com.android.server.PackageWatchdog;
import com.android.server.ServiceThread;
import com.android.server.SystemConfig;
import com.android.server.ThreadPriorityBooster;
@@ -214,6 +213,7 @@
import com.android.server.art.model.DeleteResult;
import com.android.server.compat.CompatChange;
import com.android.server.compat.PlatformCompat;
+import com.android.server.crashrecovery.CrashRecoveryAdaptor;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.Settings.VersionInfo;
import com.android.server.pm.dex.ArtManagerService;
@@ -3048,7 +3048,7 @@
mDexManager.writePackageDexUsageNow();
mDynamicCodeLogger.writeNow();
if (!refactorCrashrecovery()) {
- PackageWatchdog.getInstance(mContext).writeNow();
+ CrashRecoveryAdaptor.packageWatchdogWriteNow(mContext);
}
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java
index 685ab3a..ab756f2 100644
--- a/services/core/java/com/android/server/rollback/Rollback.java
+++ b/services/core/java/com/android/server/rollback/Rollback.java
@@ -54,7 +54,7 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
-import com.android.server.RescueParty;
+import com.android.server.crashrecovery.CrashRecoveryAdaptor;
import com.android.server.pm.pkg.AndroidPackage;
import java.io.File;
@@ -627,7 +627,7 @@
if (!deprecateFlagsAndSettingsResets()) {
// Clear flags.
- RescueParty.resetDeviceConfigForPackages(packageNames);
+ CrashRecoveryAdaptor.rescuePartyResetDeviceConfigForPackages(packageNames);
}
Consumer<Intent> onResult = result -> {
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 8f28f59..6067a99 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -384,16 +384,19 @@
}
final boolean serverVisibleChanged = mServerVisible != isServerVisible;
setServerVisible(isServerVisible);
- final boolean positionChanged = updateInsetsControlPosition(windowState);
- if (mControl != null && !positionChanged
- // The insets hint would be updated if the position is changed. Here updates it for
- // the possible change of the bounds or the server visibility.
- && (updateInsetsHint()
- || serverVisibleChanged
- && android.view.inputmethod.Flags.refactorInsetsController())) {
- // Only call notifyControlChanged here when the position is not changed. Otherwise, it
- // is called or is scheduled to be called during updateInsetsControlPosition.
- mStateController.notifyControlChanged(mControlTarget, this);
+ if (mControl != null) {
+ final boolean positionChanged = updateInsetsControlPosition(windowState);
+ if (!(positionChanged || mHasPendingPosition)
+ // The insets hint would be updated while changing the position. Here updates it
+ // for the possible change of the bounds or the server visibility.
+ && (updateInsetsHint()
+ || (android.view.inputmethod.Flags.refactorInsetsController()))
+ && serverVisibleChanged) {
+ // Only call notifyControlChanged here when the position hasn't been or won't be
+ // changed. Otherwise, it has been called or scheduled to be called during
+ // updateInsetsControlPosition.
+ mStateController.notifyControlChanged(mControlTarget, this);
+ }
}
}
@@ -409,6 +412,7 @@
mPosition.set(position.x, position.y);
if (windowState != null && windowState.getWindowFrames().didFrameSizeChange()
&& windowState.mWinAnimator.getShown() && mWindowContainer.okToDisplay()) {
+ mHasPendingPosition = true;
windowState.applyWithNextDraw(mSetControlPositionConsumer);
} else {
Transaction t = mWindowContainer.getSyncTransaction();
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 7ea1dcd..b9727f9 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -159,7 +159,7 @@
import com.android.server.contextualsearch.ContextualSearchManagerService;
import com.android.server.coverage.CoverageService;
import com.android.server.cpu.CpuMonitorService;
-import com.android.server.crashrecovery.CrashRecoveryModule;
+import com.android.server.crashrecovery.CrashRecoveryAdaptor;
import com.android.server.credentials.CredentialManagerService;
import com.android.server.criticalevents.CriticalEventLog;
import com.android.server.devicepolicy.DevicePolicyManagerService;
@@ -1230,12 +1230,12 @@
if (!Flags.refactorCrashrecovery()) {
// Initialize RescueParty.
- RescueParty.registerHealthObserver(mSystemContext);
+ CrashRecoveryAdaptor.rescuePartyRegisterHealthObserver(mSystemContext);
if (!Flags.recoverabilityDetection()) {
// Now that we have the bare essentials of the OS up and running, take
// note that we just booted, which might send out a rescue party if
// we're stuck in a runtime restart loop.
- PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ CrashRecoveryAdaptor.packageWatchdogNoteBoot(mSystemContext);
}
}
@@ -1617,7 +1617,7 @@
mSystemServiceManager.startService(ROLE_SERVICE_CLASS);
t.traceEnd();
- if (android.app.supervision.flags.Flags.supervisionApi()) {
+ if (!isWatch && android.app.supervision.flags.Flags.supervisionApi()) {
t.traceBegin("StartSupervisionService");
mSystemServiceManager.startService(SupervisionService.Lifecycle.class);
t.traceEnd();
@@ -2979,7 +2979,7 @@
if (Flags.refactorCrashrecovery()) {
t.traceBegin("StartCrashRecoveryModule");
- mSystemServiceManager.startService(CrashRecoveryModule.Lifecycle.class);
+ CrashRecoveryAdaptor.initializeCrashrecoveryModuleService(mSystemServiceManager);
t.traceEnd();
} else {
if (Flags.recoverabilityDetection()) {
@@ -2987,7 +2987,7 @@
// with package watchdog.
// Note that we just booted, which might send out a rescue party if we're stuck in a
// runtime restart loop.
- PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ CrashRecoveryAdaptor.packageWatchdogNoteBoot(mSystemContext);
}
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodMenuControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodMenuControllerTest.java
new file mode 100644
index 0000000..02dc86b
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodMenuControllerTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import static com.android.server.inputmethod.InputMethodMenuControllerNew.getMenuItems;
+import static com.android.server.inputmethod.InputMethodMenuControllerNew.getSelectedIndex;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingControllerTest.addTestImeSubtypeListItems;
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.inputmethod.Flags;
+
+import com.android.server.inputmethod.InputMethodMenuControllerNew.DividerItem;
+import com.android.server.inputmethod.InputMethodMenuControllerNew.HeaderItem;
+import com.android.server.inputmethod.InputMethodMenuControllerNew.SubtypeItem;
+import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+public class InputMethodMenuControllerTest {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ /** Verifies that getMenuItems maintains the same order and information from the given items. */
+ @Test
+ public void testGetMenuItems() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var menuItems = getMenuItems(items);
+
+ int itemsIndex = 0;
+
+ for (int i = 0; i < menuItems.size(); i++) {
+ final var menuItem = menuItems.get(i);
+ if (menuItem instanceof SubtypeItem subtypeItem) {
+ final var item = items.get(itemsIndex);
+
+ assertWithMessage("IME name does not match").that(subtypeItem.mImeName)
+ .isEqualTo(item.mImeName);
+ assertWithMessage("Subtype name does not match").that(subtypeItem.mSubtypeName)
+ .isEqualTo(item.mSubtypeName);
+ assertWithMessage("InputMethodInfo does not match").that(subtypeItem.mImi)
+ .isEqualTo(item.mImi);
+ assertWithMessage("Subtype index does not match").that(subtypeItem.mSubtypeIndex)
+ .isEqualTo(item.mSubtypeIndex);
+
+ itemsIndex++;
+ }
+ }
+
+ assertWithMessage("Items list was not fully traversed").that(itemsIndex)
+ .isEqualTo(items.size());
+ }
+
+ /**
+ * Verifies that getMenuItems does not add a header or divider if all the items belong to
+ * a single input method.
+ */
+ @Test
+ public void testGetMenuItemsNoHeaderOrDividerForSingleInputMethod() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var menuItems = getMenuItems(items);
+
+ assertThat(menuItems.stream()
+ .filter(item -> item instanceof HeaderItem || item instanceof DividerItem).toList())
+ .isEmpty();
+ }
+
+ /**
+ * Verifies that getMenuItems only adds headers for item groups with at least two items,
+ * or with a single item with a subtype name.
+ */
+ @Test
+ public void testGetMenuItemsHeaders() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "DefaultIme", "DefaultIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "ItalianIme", "ItalianIme",
+ List.of("it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var menuItems = getMenuItems(items);
+
+ assertWithMessage("Must have menu items").that(menuItems).isNotEmpty();
+
+ final var headersAndDividers = menuItems.stream()
+ .filter(item -> item instanceof HeaderItem || item instanceof DividerItem)
+ .toList();
+
+ assertWithMessage("Must have header and divider items").that(headersAndDividers).hasSize(5);
+
+ assertWithMessage("First group has no header")
+ .that(menuItems.getFirst()).isInstanceOf(SubtypeItem.class);
+ assertWithMessage("Group with multiple items has divider")
+ .that(headersAndDividers.get(0)).isInstanceOf(DividerItem.class);
+ assertWithMessage("Group with multiple items has header")
+ .that(headersAndDividers.get(1)).isInstanceOf(HeaderItem.class);
+ assertWithMessage("Group with single item with subtype name has divider")
+ .that(headersAndDividers.get(2)).isInstanceOf(DividerItem.class);
+ assertWithMessage("Group with single item with subtype name has header")
+ .that(headersAndDividers.get(3)).isInstanceOf(HeaderItem.class);
+ assertWithMessage("Group with single item without subtype name has divider only")
+ .that(headersAndDividers.get(4)).isInstanceOf(DividerItem.class);
+ }
+
+ /** Verifies that getMenuItems adds a divider before every header except the first one. */
+ @Test
+ public void testGetMenuItemsDivider() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "ItalianIme", "ItalianIme",
+ List.of("it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var menuItems = getMenuItems(items);
+
+ assertWithMessage("First item is a header")
+ .that(menuItems.getFirst()).isInstanceOf(HeaderItem.class);
+ assertWithMessage("Last item is a subtype")
+ .that(menuItems.getLast()).isInstanceOf(SubtypeItem.class);
+
+ for (int i = 0; i < menuItems.size(); i++) {
+ final var item = menuItems.get(i);
+ if (item instanceof HeaderItem && i > 0) {
+ final var prevItem = menuItems.get(i - 1);
+ assertWithMessage("The item before a header should be a divider")
+ .that(prevItem).isInstanceOf(DividerItem.class);
+ } else if (item instanceof DividerItem && i < menuItems.size() - 1) {
+ final var nextItem = menuItems.get(i + 1);
+ assertWithMessage("The item after a divider should be a header or subtype")
+ .that(nextItem instanceof HeaderItem || nextItem instanceof SubtypeItem)
+ .isTrue();
+ }
+ }
+ }
+
+ /**
+ * Verifies that getSelectedIndex returns the matching item when the selected subtype is given.
+ */
+ @Test
+ public void testGetSelectedIndexWithSelectedSubtype() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ List.of("it", "jp", "pt"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var simpleImeId = items.get(2).mImi.getId();
+ final var menuItems = getMenuItems(items);
+
+ final int selectedIndex = getSelectedIndex(menuItems, simpleImeId, 1);
+ // Two headers + one divider + three items
+ assertThat(selectedIndex).isEqualTo(6);
+ }
+
+ /**
+ * Verifies that getSelectedIndex returns the first item of the selected input method,
+ * when no selected subtype is given.
+ */
+ @Test
+ public void testGetSelectedIndexWithoutSelectedSubtype() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ List.of("it", "jp", "pt"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var simpleImeId = items.get(2).mImi.getId();
+ final var menuItems = getMenuItems(items);
+
+ final int selectedIndex = getSelectedIndex(menuItems, simpleImeId, NOT_A_SUBTYPE_INDEX);
+
+ // Two headers + one divider + two items
+ assertThat(selectedIndex).isEqualTo(5);
+ }
+
+ /**
+ * Verifies that getSelectedIndex will return the item of the selected input method that has
+ * no subtype, when this is the first one reached, regardless of the given selected subtype.
+ */
+ @Test
+ public void getSelectedIndexNoSubtype() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var simpleImeId = items.get(2).mImi.getId();
+ final var menuItems = getMenuItems(items);
+
+ final int selectedIndex = getSelectedIndex(menuItems, simpleImeId, 1);
+
+ // One header + one divider + two items
+ assertThat(selectedIndex).isEqualTo(4);
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index 770451c..a804f24 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -75,7 +75,7 @@
.build();
}
- private static void addTestImeSubtypeListItems(@NonNull List<ImeSubtypeListItem> items,
+ static void addTestImeSubtypeListItems(@NonNull List<ImeSubtypeListItem> items,
@NonNull String imeName, @NonNull String imeLabel,
@Nullable List<String> subtypeLocales, boolean supportsSwitchingToNextInputMethod) {
final ApplicationInfo ai = new ApplicationInfo();
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 390eb93..2fe6918 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -181,12 +181,14 @@
Intent.ACTION_USER_STARTING);
private static final Set<Integer> START_FOREGROUND_USER_MESSAGE_CODES = newHashSet(
+ 0, // for startUserInternalOnHandler
REPORT_USER_SWITCH_MSG,
USER_SWITCH_TIMEOUT_MSG,
USER_START_MSG,
USER_CURRENT_MSG);
private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
+ 0, // for startUserInternalOnHandler
USER_START_MSG,
REPORT_LOCKED_BOOT_COMPLETE_MSG);
@@ -374,7 +376,7 @@
// and the cascade effect goes on...). In fact, a better approach would to not assert the
// binder calls, but their side effects (in this case, that the user is stopped right away)
assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes())
- .containsExactly(USER_START_MSG);
+ .containsExactly(/* for startUserInternalOnHandler */ 0, USER_START_MSG);
}
private void startUserAssertions(
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 8a1bb00..e386808 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -116,7 +116,6 @@
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.compatibility.common.util.SystemUtil;
import com.android.internal.app.BlockedAppStreamingActivity;
import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
@@ -559,6 +558,68 @@
}
@Test
+ public void deviceOwner_cannotMessWithAnotherDeviceTheyDoNotOwn() {
+ VirtualDeviceImpl unownedDevice =
+ createVirtualDevice(VIRTUAL_DEVICE_ID_2, DEVICE_OWNER_UID_2);
+
+ // The arguments don't matter, the owner uid check is always the first statement.
+ assertThrows(SecurityException.class, () -> unownedDevice.goToSleep());
+ assertThrows(SecurityException.class, () -> unownedDevice.wakeUp());
+
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.launchPendingIntent(0, null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.registerIntentInterceptor(null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.unregisterIntentInterceptor(null));
+
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.addActivityPolicyExemption(null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.removeActivityPolicyExemption(null));
+ assertThrows(SecurityException.class, () -> unownedDevice.setDevicePolicy(0, 0));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.setDevicePolicyForDisplay(0, 0, 0));
+ assertThrows(SecurityException.class, () -> unownedDevice.setDisplayImePolicy(0, 0));
+
+ assertThrows(SecurityException.class, () -> unownedDevice.registerVirtualCamera(null));
+ assertThrows(SecurityException.class, () -> unownedDevice.unregisterVirtualCamera(null));
+
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.onAudioSessionStarting(0, null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.onAudioSessionEnded());
+
+ assertThrows(SecurityException.class, () -> unownedDevice.createVirtualDisplay(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.createVirtualDpad(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.createVirtualMouse(null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.createVirtualTouchscreen(null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.createVirtualNavigationTouchpad(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.createVirtualStylus(null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.createVirtualRotaryEncoder(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.unregisterInputDevice(null));
+
+ assertThrows(SecurityException.class, () -> unownedDevice.sendDpadKeyEvent(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.sendKeyEvent(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.sendButtonEvent(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.sendTouchEvent(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.sendRelativeEvent(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.sendScrollEvent(null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.sendStylusMotionEvent(null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.sendStylusButtonEvent(null, null));
+ assertThrows(SecurityException.class,
+ () -> unownedDevice.sendRotaryEncoderScrollEvent(null, null));
+ assertThrows(SecurityException.class, () -> unownedDevice.setShowPointerIcon(true));
+
+ assertThrows(SecurityException.class, () -> unownedDevice.getVirtualSensorList());
+ assertThrows(SecurityException.class, () -> unownedDevice.sendSensorEvent(null, null));
+ }
+
+ @Test
public void getDeviceOwnerUid_oneDevice_returnsCorrectId() {
int ownerUid = mLocalService.getDeviceOwnerUid(mDeviceImpl.getDeviceId());
assertThat(ownerUid).isEqualTo(mDeviceImpl.getOwnerUid());
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 d64b9e8..404ede6 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -18,6 +18,7 @@
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_DEFAULT;
import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
+import static android.app.Flags.FLAG_MODES_UI;
import static android.app.Notification.VISIBILITY_PRIVATE;
import static android.app.Notification.VISIBILITY_SECRET;
import static android.app.NotificationChannel.ALLOW_BUBBLE_ON;
@@ -81,6 +82,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -248,7 +250,7 @@
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
return FlagsParameterization.allCombinationsOf(
- FLAG_NOTIFICATION_CLASSIFICATION);
+ FLAG_NOTIFICATION_CLASSIFICATION, FLAG_MODES_UI);
}
public PreferencesHelperTest(FlagsParameterization flags) {
@@ -2701,7 +2703,11 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean());
+ } else {
+ verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
// create notification channel that can bypass dnd
@@ -2711,18 +2717,30 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
assertTrue(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
// delete channels
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false);
assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND
- verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean());
+ } else {
+ verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2738,7 +2756,11 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean());
+ } else {
+ verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
// Recreate a channel & now the app has dnd access granted and can set the bypass dnd field
@@ -2748,7 +2770,11 @@
uid, false);
assertTrue(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2764,7 +2790,11 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean());
+ } else {
+ verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
// create notification channel that can bypass dnd, using local app level settings
@@ -2774,18 +2804,30 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
assertTrue(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
// delete channels
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false);
assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND
- verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean());
+ } else {
+ verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2812,7 +2854,11 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2834,7 +2880,11 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2856,7 +2906,11 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2872,7 +2926,11 @@
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean());
+ } else {
+ verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
// update channel so it CAN bypass dnd:
@@ -2880,7 +2938,11 @@
channel.setBypassDnd(true);
mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true);
assertTrue(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
// update channel so it can't bypass dnd:
@@ -2888,7 +2950,11 @@
channel.setBypassDnd(false);
mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2901,7 +2967,11 @@
when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
mHelper.syncChannelsBypassingDnd();
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false));
+ } else {
+ verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
@@ -2911,7 +2981,11 @@
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0, 0);
when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
assertFalse(mHelper.areChannelsBypassingDnd());
- verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ if (android.app.Flags.modesUi()) {
+ verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean());
+ } else {
+ verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt());
+ }
resetZenModeHelper();
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 294027b..8b3ac2b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -7027,6 +7027,29 @@
ZenModeConfig.EVENTS_OBSOLETE_RULE_ID);
}
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void updateHasPriorityChannels_keepsChannelSettings() {
+ setupZenConfig();
+
+ // Set priority channels setting on manual mode to confirm that it is unaffected by changes
+ // to the state describing the existence of such channels.
+ mZenModeHelper.mConfig.manualRule.zenPolicy =
+ new ZenPolicy.Builder(mZenModeHelper.mConfig.manualRule.zenPolicy)
+ .allowPriorityChannels(false)
+ .build();
+
+ mZenModeHelper.updateHasPriorityChannels(true);
+ assertThat(mZenModeHelper.getNotificationPolicy().hasPriorityChannels()).isTrue();
+
+ // getNotificationPolicy() gets its policy from the manual rule; channels not permitted
+ assertThat(mZenModeHelper.getNotificationPolicy().allowPriorityChannels()).isFalse();
+
+ mZenModeHelper.updateHasPriorityChannels(false);
+ assertThat(mZenModeHelper.getNotificationPolicy().hasPriorityChannels()).isFalse();
+ assertThat(mZenModeHelper.getNotificationPolicy().allowPriorityChannels()).isFalse();
+ }
+
private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
@Nullable ZenPolicy zenPolicy) {
ZenRule rule = new ZenRule();