Merge "UsbService:update mUsbDisableRequesters for enableUsbDataWhileDocked()" into main
diff --git a/Ravenwood.bp b/Ravenwood.bp
index c3b22c4..7c7c0e2 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -168,6 +168,7 @@
"services.core.ravenwood",
],
jarjar_rules: ":ravenwood-services-jarjar-rules",
+ visibility: ["//visibility:private"],
}
java_library {
@@ -179,6 +180,7 @@
"services.core.ravenwood",
],
jarjar_rules: ":ravenwood-services-jarjar-rules",
+ visibility: ["//visibility:private"],
}
java_library {
diff --git a/api/coverage/tools/ExtractFlaggedApis.kt b/api/coverage/tools/ExtractFlaggedApis.kt
index caa1929..d5adfd0 100644
--- a/api/coverage/tools/ExtractFlaggedApis.kt
+++ b/api/coverage/tools/ExtractFlaggedApis.kt
@@ -16,51 +16,69 @@
package android.platform.coverage
+import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.text.ApiFile
import java.io.File
import java.io.FileWriter
/** Usage: extract-flagged-apis <api text file> <output .pb file> */
fun main(args: Array<String>) {
- var cb = ApiFile.parseApi(listOf(File(args[0])))
- var builder = FlagApiMap.newBuilder()
+ val cb = ApiFile.parseApi(listOf(File(args[0])))
+ val builder = FlagApiMap.newBuilder()
for (pkg in cb.getPackages().packages) {
- var packageName = pkg.qualifiedName()
+ val packageName = pkg.qualifiedName()
pkg.allClasses()
.filter { it.methods().size > 0 }
.forEach {
- for (method in it.methods()) {
- val flagValue =
- method.modifiers
- .findAnnotation("android.annotation.FlaggedApi")
- ?.findAttribute("value")
- ?.value
- ?.value()
- if (flagValue != null && flagValue is String) {
- var api =
- JavaMethod.newBuilder()
- .setPackageName(packageName)
- .setClassName(it.fullName())
- .setMethodName(method.name())
- for (param in method.parameters()) {
- api.addParameters(param.type().toTypeString())
- }
- if (builder.containsFlagToApi(flagValue)) {
- var updatedApis =
- builder
- .getFlagToApiOrThrow(flagValue)
- .toBuilder()
- .addJavaMethods(api)
- .build()
- builder.putFlagToApi(flagValue, updatedApis)
- } else {
- var apis = FlaggedApis.newBuilder().addJavaMethods(api).build()
- builder.putFlagToApi(flagValue, apis)
- }
- }
- }
+ extractFlaggedApisFromClass(it, it.methods(), packageName, builder)
+ extractFlaggedApisFromClass(it, it.constructors(), packageName, builder)
}
}
val flagApiMap = builder.build()
FileWriter(args[1]).use { it.write(flagApiMap.toString()) }
}
+
+fun extractFlaggedApisFromClass(
+ classItem: ClassItem,
+ methods: List<MethodItem>,
+ packageName: String,
+ builder: FlagApiMap.Builder
+) {
+ val classFlag =
+ classItem.modifiers
+ .findAnnotation("android.annotation.FlaggedApi")
+ ?.findAttribute("value")
+ ?.value
+ ?.value() as? String
+ for (method in methods) {
+ val methodFlag =
+ method.modifiers
+ .findAnnotation("android.annotation.FlaggedApi")
+ ?.findAttribute("value")
+ ?.value
+ ?.value() as? String
+ ?: classFlag
+ val api =
+ JavaMethod.newBuilder()
+ .setPackageName(packageName)
+ .setClassName(classItem.fullName())
+ .setMethodName(method.name())
+ for (param in method.parameters()) {
+ api.addParameters(param.type().toTypeString())
+ }
+ if (methodFlag != null) {
+ addFlaggedApi(builder, api, methodFlag)
+ }
+ }
+}
+
+fun addFlaggedApi(builder: FlagApiMap.Builder, api: JavaMethod.Builder, flag: String) {
+ if (builder.containsFlagToApi(flag)) {
+ val updatedApis = builder.getFlagToApiOrThrow(flag).toBuilder().addJavaMethods(api).build()
+ builder.putFlagToApi(flag, updatedApis)
+ } else {
+ val apis = FlaggedApis.newBuilder().addJavaMethods(api).build()
+ builder.putFlagToApi(flag, apis)
+ }
+}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 40ee57e..f36aeab 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -486,12 +486,16 @@
}
public final class UiAutomation {
+ method public void addOverridePermissionState(int, @NonNull String, int);
+ method public void clearAllOverridePermissionStates();
+ method public void clearOverridePermissionStates(int);
method public void destroy();
method @NonNull public java.util.Set<java.lang.String> getAdoptedShellPermissions();
method @Deprecated public boolean grantRuntimePermission(String, String, android.os.UserHandle);
method public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean);
method public void injectInputEventToInputFilter(@NonNull android.view.InputEvent);
method public boolean isNodeInCache(@NonNull android.view.accessibility.AccessibilityNodeInfo);
+ method public void removeOverridePermissionState(int, @NonNull String);
method @Deprecated public boolean revokeRuntimePermission(String, String, android.os.UserHandle);
method public void syncInputTransactions();
method public void syncInputTransactions(boolean);
diff --git a/core/java/android/app/AppOpsManagerInternal.java b/core/java/android/app/AppOpsManagerInternal.java
index 8daee58..f8a8f5d 100644
--- a/core/java/android/app/AppOpsManagerInternal.java
+++ b/core/java/android/app/AppOpsManagerInternal.java
@@ -53,9 +53,10 @@
* @param superImpl The super implementation.
* @return The app op check result.
*/
- int checkOperation(int code, int uid, String packageName, @Nullable String attributionTag,
- int virtualDeviceId, boolean raw, HexFunction<Integer, Integer, String, String,
- Integer, Boolean, Integer> superImpl);
+ int checkOperation(int code, int uid, @Nullable String packageName,
+ @Nullable String attributionTag, int virtualDeviceId, boolean raw,
+ @NonNull HexFunction<Integer, Integer, String, String, Integer, Boolean, Integer>
+ superImpl);
/**
* Allows overriding check audio operation behavior.
@@ -67,8 +68,8 @@
* @param superImpl The super implementation.
* @return The app op check result.
*/
- int checkAudioOperation(int code, int usage, int uid, String packageName,
- QuadFunction<Integer, Integer, Integer, String, Integer> superImpl);
+ int checkAudioOperation(int code, int usage, int uid, @Nullable String packageName,
+ @NonNull QuadFunction<Integer, Integer, Integer, String, Integer> superImpl);
/**
* Allows overriding note operation behavior.
@@ -125,7 +126,7 @@
* @param superImpl The super implementation.
* @return The app op note result.
*/
- SyncNotedAppOp startOperation(IBinder token, int code, int uid,
+ SyncNotedAppOp startOperation(@NonNull IBinder token, int code, int uid,
@Nullable String packageName, @Nullable String attributionTag, int virtualDeviceId,
boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp,
@Nullable String message, boolean shouldCollectMessage,
@@ -152,8 +153,9 @@
*/
SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
@NonNull AttributionSource attributionSource, boolean startIfModeDefault,
- boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
- boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
+ boolean shouldCollectAsyncNotedOp, @Nullable String message,
+ boolean shouldCollectMessage, boolean skipProxyOperation,
+ @AttributionFlags int proxyAttributionFlags,
@AttributionFlags int proxiedAttributionFlags, int attributionChainId,
@NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean,
Boolean, String, Boolean, Boolean, Integer, Integer, Integer,
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 84bc6ce..85611e8 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -970,4 +970,40 @@
* time in the past.
*/
long getUidLastIdleElapsedTime(int uid, in String callingPackage);
+
+ /**
+ * Adds permission to be overridden to the given state. Must be called from root user.
+ *
+ * @param originatingUid The UID of the instrumented app that initialized the override
+ * @param uid The UID of the app whose permission will be overridden
+ * @param permission The permission whose state will be overridden
+ * @param result The state to override the permission to
+ *
+ * @see PackageManager.PermissionResult
+ */
+ void addOverridePermissionState(int originatingUid, int uid, String permission, int result);
+
+ /**
+ * Removes overridden permission. Must be called from root user.
+ *
+ * @param originatingUid The UID of the instrumented app that initialized the override
+ * @param uid The UID of the app whose permission is overridden
+ * @param permission The permission whose state will no longer be overridden
+ */
+ void removeOverridePermissionState(int originatingUid, int uid, String permission);
+
+ /**
+ * Clears all overridden permissions for the given UID. Must be called from root user.
+ *
+ * @param originatingUid The UID of the instrumented app that initialized the override
+ * @param uid The UID of the app whose permissions will no longer be overridden
+ */
+ void clearOverridePermissionStates(int originatingUid, int uid);
+
+ /**
+ * Clears all overridden permissions on the device. Must be called from root user.
+ *
+ * @param originatingUid The UID of the instrumented app that initialized the override
+ */
+ void clearAllOverridePermissionStates(int originatingUid);
}
diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl
index 63cae63..69c3bd3 100644
--- a/core/java/android/app/IUiAutomationConnection.aidl
+++ b/core/java/android/app/IUiAutomationConnection.aidl
@@ -62,4 +62,8 @@
void executeShellCommandWithStderr(String command, in ParcelFileDescriptor sink,
in ParcelFileDescriptor source, in ParcelFileDescriptor stderrSink);
List<String> getAdoptedShellPermissions();
+ void addOverridePermissionState(int uid, String permission, int result);
+ void removeOverridePermissionState(int uid, String permission);
+ void clearOverridePermissionStates(int uid);
+ void clearAllOverridePermissionStates();
}
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index b0edc3d..348d4d8f 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -33,6 +33,7 @@
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
@@ -653,6 +654,81 @@
}
/**
+ * Adds permission to be overridden to the given state. UiAutomation must be connected to
+ * root user.
+ *
+ * @param uid The UID of the app whose permission will be overridden
+ * @param permission The permission whose state will be overridden
+ * @param result The state to override the permission to
+ *
+ * @see PackageManager#PERMISSION_GRANTED
+ * @see PackageManager#PERMISSION_DENIED
+ *
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("UnflaggedApi")
+ public void addOverridePermissionState(int uid, @NonNull String permission,
+ @PackageManager.PermissionResult int result) {
+ try {
+ mUiAutomationConnection.addOverridePermissionState(uid, permission, result);
+ } catch (RemoteException re) {
+ re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes overridden permission. UiAutomation must be connected to root user.
+ *
+ * @param uid The UID of the app whose permission is overridden
+ * @param permission The permission whose state will no longer be overridden
+ *
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("UnflaggedApi")
+ public void removeOverridePermissionState(int uid, @NonNull String permission) {
+ try {
+ mUiAutomationConnection.removeOverridePermissionState(uid, permission);
+ } catch (RemoteException re) {
+ re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Clears all overridden permissions for the given UID. UiAutomation must be connected to
+ * root user.
+ *
+ * @param uid The UID of the app whose permissions will no longer be overridden
+ *
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("UnflaggedApi")
+ public void clearOverridePermissionStates(int uid) {
+ try {
+ mUiAutomationConnection.clearOverridePermissionStates(uid);
+ } catch (RemoteException re) {
+ re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Clears all overridden permissions on the device. UiAutomation must be connected to root user.
+ *
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("UnflaggedApi")
+ public void clearAllOverridePermissionStates() {
+ try {
+ mUiAutomationConnection.clearAllOverridePermissionStates();
+ } catch (RemoteException re) {
+ re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Performs a global action. Such an action can be performed at any moment
* regardless of the current application or user location in that application.
* For example going back, going home, opening recents, etc.
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
index 33e260f..3c4bd9e 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -437,6 +437,71 @@
}
}
+ @Override
+ public void addOverridePermissionState(int uid, String permission, int result)
+ throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mActivityManager.addOverridePermissionState(callingUid, uid, permission, result);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void removeOverridePermissionState(int uid, String permission) throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mActivityManager.removeOverridePermissionState(callingUid, uid, permission);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void clearOverridePermissionStates(int uid) throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mActivityManager.clearOverridePermissionStates(callingUid, uid);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void clearAllOverridePermissionStates() throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mActivityManager.clearAllOverridePermissionStates(callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
public class Repeater implements Runnable {
// Continuously read readFrom and write back to writeTo until EOF is encountered
private final InputStream readFrom;
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index ea7f8c4c..c6a8762 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -19,8 +19,6 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.companion.virtual.VirtualDeviceManager;
-import android.companion.virtual.camera.VirtualCameraConfig;
import android.compat.annotation.UnsupportedAppUsage;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.impl.ExtensionKey;
@@ -5349,10 +5347,9 @@
* <p>Id of the device that owns this camera.</p>
* <p>In case of a virtual camera, this would be the id of the virtual device
* owning the camera. For any other camera, this key would not be present.
- * Callers should assume {@link android.content.Context#DEVICE_ID_DEFAULT}
+ * Callers should assume {@link android.content.Context#DEVICE_ID_DEFAULT }
* in case this key is not present.</p>
* <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
- * @see VirtualDeviceManager.VirtualDevice#createVirtualCamera(VirtualCameraConfig)
* @hide
*/
public static final Key<Integer> INFO_DEVICE_ID =
diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java
index 5b711c9..2fe115f 100644
--- a/core/java/android/os/MessageQueue.java
+++ b/core/java/android/os/MessageQueue.java
@@ -25,6 +25,8 @@
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
+import dalvik.annotation.optimization.CriticalNative;
+
import java.io.FileDescriptor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -78,6 +80,7 @@
private native static void nativeDestroy(long ptr);
@UnsupportedAppUsage
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+ @CriticalNative
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index aa2f85d..b7d421a 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10225,6 +10225,13 @@
"screensaver_complications_enabled";
/**
+ * Defines the enabled state for the glanceable hub.
+ *
+ * @hide
+ */
+ public static final String GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled";
+
+ /**
* Whether home controls are enabled to be shown over the screensaver by the user.
*
* @hide
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index fd4ff29..03b57d0 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -309,6 +309,7 @@
"libdebuggerd_client",
"libutils",
"libbinder",
+ "libbinderdebug",
"libbinder_ndk",
"libui",
"libgraphicsenv",
diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp
index 9593fe58..3c2dccd 100644
--- a/core/jni/android_os_Debug.cpp
+++ b/core/jni/android_os_Debug.cpp
@@ -16,44 +16,46 @@
#define LOG_TAG "android.os.Debug"
+#include "android_os_Debug.h"
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
#include <assert.h>
+#include <binderdebug/BinderDebug.h>
+#include <bionic/malloc.h>
#include <ctype.h>
+#include <debuggerd/client.h>
+#include <dmabufinfo/dmabuf_sysfs_stats.h>
+#include <dmabufinfo/dmabufinfo.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
+#include <log/log.h>
#include <malloc.h>
+#include <meminfo/androidprocheaps.h>
+#include <meminfo/procmeminfo.h>
+#include <meminfo/sysmeminfo.h>
+#include <memtrack/memtrack.h>
+#include <memunreachable/memunreachable.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
+#include <utils/String8.h>
+#include <utils/misc.h>
+#include <vintf/KernelConfigs.h>
#include <iomanip>
#include <string>
#include <vector>
-#include <android-base/logging.h>
-#include <android-base/properties.h>
-#include <bionic/malloc.h>
-#include <debuggerd/client.h>
-#include <log/log.h>
-#include <utils/misc.h>
-#include <utils/String8.h>
-
-#include <nativehelper/JNIPlatformHelp.h>
-#include <nativehelper/ScopedUtfChars.h>
#include "jni.h"
-#include <dmabufinfo/dmabuf_sysfs_stats.h>
-#include <dmabufinfo/dmabufinfo.h>
-#include <meminfo/androidprocheaps.h>
-#include <meminfo/procmeminfo.h>
-#include <meminfo/sysmeminfo.h>
-#include <memtrack/memtrack.h>
-#include <memunreachable/memunreachable.h>
-#include <android-base/strings.h>
-#include "android_os_Debug.h"
-#include <vintf/KernelConfigs.h>
namespace android
{
@@ -579,6 +581,15 @@
return false;
}
+ std::string binderState;
+ android::status_t status = android::getBinderTransactions(pid, binderState);
+ if (status == android::OK) {
+ if (!android::base::WriteStringToFd(binderState, fd)) {
+ PLOG(ERROR) << "Failed to dump binder state info for pid: " << pid;
+ }
+ } else {
+ PLOG(ERROR) << "Failed to get binder state info for pid: " << pid << " status: " << status;
+ }
int res = dump_backtrace_to_file_timeout(pid, dumpType, timeoutSecs, fd);
if (fdatasync(fd.get()) != 0) {
PLOG(ERROR) << "Failed flushing trace.";
diff --git a/core/jni/android_os_MessageQueue.cpp b/core/jni/android_os_MessageQueue.cpp
index 30d9ea1..9525605 100644
--- a/core/jni/android_os_MessageQueue.cpp
+++ b/core/jni/android_os_MessageQueue.cpp
@@ -225,7 +225,7 @@
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
-static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
+static void android_os_MessageQueue_nativeWake(jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 99a00b8..120d681 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode
+import android.graphics.Rect
import android.graphics.Region
import android.util.ArrayMap
import android.util.ArraySet
@@ -55,6 +56,8 @@
private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
// Track corner/caption regions of desktop tasks, used to determine gesture exclusion
private val desktopExclusionRegions = SparseArray<Region>()
+ // Track last bounds of task before toggled to stable bounds
+ private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>()
private var desktopGestureExclusionListener: Consumer<Region>? = null
private var desktopGestureExclusionExecutor: Executor? = null
@@ -307,6 +310,7 @@
taskId
)
freeformTasksInZOrder.remove(taskId)
+ boundsBeforeMaximizeByTaskId.remove(taskId)
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
"DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString()
@@ -358,6 +362,20 @@
}
/**
+ * Removes and returns the bounds saved before maximizing the given task.
+ */
+ fun removeBoundsBeforeMaximize(taskId: Int): Rect? {
+ return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId)
+ }
+
+ /**
+ * Saves the bounds of the given task before maximizing.
+ */
+ fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) {
+ boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
+ }
+
+ /**
* Check if display with id [displayId] has desktop tasks stashed
*/
fun isStashed(displayId: Int): Boolean {
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 c369061..e210ea7 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
@@ -120,7 +120,6 @@
private var visualIndicator: DesktopModeVisualIndicator? = null
private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
DesktopModeShellCommandHandler(this)
-
private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> {
t: SurfaceControl.Transaction ->
visualIndicator?.releaseVisualIndicator(t)
@@ -570,7 +569,10 @@
}
}
- /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */
+ /**
+ * Quick-resizes a desktop task, toggling between the stable bounds and the last saved bounds
+ * if available or the default bounds otherwise.
+ */
fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
@@ -578,11 +580,21 @@
displayLayout.getStableBounds(stableBounds)
val destinationBounds = Rect()
if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) {
- // The desktop task is currently occupying the whole stable bounds, toggle to the
- // default bounds.
- getDefaultDesktopTaskBounds(displayLayout, destinationBounds)
+ // The desktop task is currently occupying the whole stable bounds. If the bounds
+ // before the task was toggled to stable bounds were saved, toggle the task to those
+ // bounds. Otherwise, toggle to the default bounds.
+ val taskBoundsBeforeMaximize =
+ desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
+ if (taskBoundsBeforeMaximize != null) {
+ destinationBounds.set(taskBoundsBeforeMaximize)
+ } else {
+ getDefaultDesktopTaskBounds(displayLayout, destinationBounds)
+ }
} else {
- // Toggle to the stable bounds.
+ // Save current bounds so that task can be restored back to original bounds if necessary
+ // and toggle to the stable bounds.
+ val taskBounds = taskInfo.configuration.windowConfiguration.bounds
+ desktopModeTaskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, taskBounds)
destinationBounds.set(stableBounds)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index b830a41..0061d03 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -369,67 +369,50 @@
val startBounds = draggedTaskChange.startAbsBounds
val endBounds = draggedTaskChange.endAbsBounds
- // TODO(b/301106941): Instead of forcing-finishing the animation that scales the
- // surface down and then starting another that scales it back up to the final size,
- // blend the two animations.
- state.dragAnimator.endAnimator()
- // Using [DRAG_FREEFORM_SCALE] to calculate animated width/height is possible because
- // it is known that the animation scale is finished because the animation was
- // force-ended above. This won't be true when the two animations are blended.
- val animStartWidth = (startBounds.width() * DRAG_FREEFORM_SCALE).toInt()
- val animStartHeight = (startBounds.height() * DRAG_FREEFORM_SCALE).toInt()
- // Using end bounds here to find the left/top also assumes the center animation has
- // finished and the surface is placed exactly in the center of the screen which matches
- // the end/default bounds of the now freeform task.
- val animStartLeft = endBounds.centerX() - (animStartWidth / 2)
- val animStartTop = endBounds.centerY() - (animStartHeight / 2)
- val animStartBounds = Rect(
- animStartLeft,
- animStartTop,
- animStartLeft + animStartWidth,
- animStartTop + animStartHeight
+ // Pause any animation that may be currently playing; we will use the relevant
+ // details of that animation here.
+ state.dragAnimator.cancelAnimator()
+ // We still apply scale to task bounds; as we animate the bounds to their
+ // end value, animate scale to 1.
+ val startScale = state.dragAnimator.scale
+ val startPosition = state.dragAnimator.position
+ val unscaledStartWidth = startBounds.width()
+ val unscaledStartHeight = startBounds.height()
+ val unscaledStartBounds = Rect(
+ startPosition.x.toInt(),
+ startPosition.y.toInt(),
+ startPosition.x.toInt() + unscaledStartWidth,
+ startPosition.y.toInt() + unscaledStartHeight
)
-
dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t)
- t.apply {
- setScale(draggedTaskLeash, 1f, 1f)
- setPosition(
- draggedTaskLeash,
- animStartBounds.left.toFloat(),
- animStartBounds.top.toFloat()
- )
- setWindowCrop(
- draggedTaskLeash,
- animStartBounds.width(),
- animStartBounds.height()
- )
- }
// Accept the merge by applying the merging transaction (applied by #showResizeVeil)
// and finish callback. Show the veil and position the task at the first frame before
// starting the final animation.
- onTaskResizeAnimationListener.onAnimationStart(state.draggedTaskId, t, animStartBounds)
+ onTaskResizeAnimationListener.onAnimationStart(state.draggedTaskId, t,
+ unscaledStartBounds)
finishCallback.onTransitionFinished(null /* wct */)
- // Because the task surface was scaled down during the drag, we must use the animated
- // bounds instead of the [startAbsBounds].
val tx: SurfaceControl.Transaction = transactionSupplier.get()
- ValueAnimator.ofObject(rectEvaluator, animStartBounds, endBounds)
+ ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds)
.setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
.apply {
addUpdateListener { animator ->
val animBounds = animator.animatedValue as Rect
+ val animFraction = animator.animatedFraction
+ // Progress scale from starting value to 1 as animation plays.
+ val animScale = startScale + animFraction * (1 - startScale)
tx.apply {
- setScale(draggedTaskLeash, 1f, 1f)
- setPosition(
- draggedTaskLeash,
- animBounds.left.toFloat(),
- animBounds.top.toFloat()
- )
+ setScale(draggedTaskLeash, animScale, animScale)
+ setPosition(
+ draggedTaskLeash,
+ animBounds.left.toFloat(),
+ animBounds.top.toFloat()
+ )
setWindowCrop(
- draggedTaskLeash,
- animBounds.width(),
- animBounds.height()
+ draggedTaskLeash,
+ animBounds.width(),
+ animBounds.height()
)
}
onTaskResizeAnimationListener.onBoundsChange(
@@ -493,10 +476,8 @@
val draggedTaskChange = state.draggedTaskChange
?: throw IllegalStateException("Expected non-null task change")
val sc = draggedTaskChange.leash
- // TODO(b/301106941): Don't end the animation and start one to scale it back, merge them
- // instead.
- // End the animation that shrinks the window when task is first dragged from fullscreen
- dragToDesktopAnimator.endAnimator()
+ // Pause the animation that shrinks the window when task is first dragged from fullscreen
+ dragToDesktopAnimator.cancelAnimator()
// Then animate the scaled window back to its original bounds.
val x: Float = dragToDesktopAnimator.position.x
val y: Float = dragToDesktopAnimator.position.y
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 708b14c..a0f9c6b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -33,16 +33,8 @@
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.compatui.AppCompatUtils.isSingleTopActivityTranslucent;
-import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR;
import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
-import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR;
-import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
-import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION;
-import static com.android.wm.shell.windowdecor.MoveToDesktopAnimator.DRAG_FREEFORM_SCALE;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
@@ -858,26 +850,18 @@
}
case MotionEvent.ACTION_UP: {
if (mTransitionDragActive) {
- final DesktopModeVisualIndicator.IndicatorType indicatorType =
- mDesktopTasksController.updateVisualIndicator(relevantDecor.mTaskInfo,
- relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY());
+ mDesktopTasksController.updateVisualIndicator(relevantDecor.mTaskInfo,
+ relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY());
mTransitionDragActive = false;
- if (indicatorType == TO_DESKTOP_INDICATOR
- || indicatorType == TO_SPLIT_LEFT_INDICATOR
- || indicatorType == TO_SPLIT_RIGHT_INDICATOR) {
- if (DesktopModeStatus.isEnabled()) {
- animateToDesktop(relevantDecor, ev);
- }
- mMoveToDesktopAnimator = null;
- return;
- } else if (mMoveToDesktopAnimator != null) {
+ if (mMoveToDesktopAnimator != null) {
// Though this isn't a hover event, we need to update handle's hover state
// as it likely will change.
relevantDecor.updateHoverAndPressStatus(ev);
mDesktopTasksController.onDragPositioningEndThroughStatusBar(
new PointF(ev.getRawX(), ev.getRawY()),
relevantDecor.mTaskInfo,
- calculateFreeformBounds(ev.getDisplayId(), DRAG_FREEFORM_SCALE));
+ calculateFreeformBounds(ev.getDisplayId(),
+ DesktopTasksController.DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
mMoveToDesktopAnimator = null;
return;
} else {
@@ -946,54 +930,6 @@
(int) (screenHeight * (adjustmentPercentage + scale)));
}
- /**
- * Blocks relayout until transition is finished and transitions to Desktop
- */
- private void animateToDesktop(DesktopModeWindowDecoration relevantDecor,
- MotionEvent ev) {
- centerAndMoveToDesktopWithAnimation(relevantDecor, ev);
- }
-
- /**
- * Animates a window to the center, grows to freeform size, and transitions to Desktop Mode.
- * @param relevantDecor the window decor of the task to be animated
- * @param ev the motion event that triggers the animation
- * TODO(b/315527000): This animation needs to be adjusted to allow snap left/right cases.
- * Currently fullscreen -> split snap still animates to center screen before readjusting.
- */
- private void centerAndMoveToDesktopWithAnimation(DesktopModeWindowDecoration relevantDecor,
- MotionEvent ev) {
- ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
- animator.setDuration(FREEFORM_ANIMATION_DURATION);
- final SurfaceControl sc = relevantDecor.mTaskSurface;
- final Rect endBounds = calculateFreeformBounds(ev.getDisplayId(), DRAG_FREEFORM_SCALE);
- final Transaction t = mTransactionFactory.get();
- final float diffX = endBounds.centerX() - ev.getRawX();
- final float diffY = endBounds.top - ev.getRawY();
- final float startingX = ev.getRawX() - DRAG_FREEFORM_SCALE
- * mDragToDesktopAnimationStartBounds.width() / 2;
-
- animator.addUpdateListener(animation -> {
- final float animatorValue = (float) animation.getAnimatedValue();
- final float x = startingX + diffX * animatorValue;
- final float y = ev.getRawY() + diffY * animatorValue;
- t.setPosition(sc, x, y);
- t.apply();
- });
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mDesktopTasksController.onDragPositioningEndThroughStatusBar(
- new PointF(ev.getRawX(), ev.getRawY()),
- relevantDecor.mTaskInfo,
- calculateFreeformBounds(ev.getDisplayId(),
- DesktopTasksController
- .DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
- }
- });
- animator.start();
- }
-
@Nullable
private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
index af05523..987aadf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
@@ -31,15 +31,16 @@
private val animatedTaskWidth
get() = dragToDesktopAnimator.animatedValue as Float * startBounds.width()
+ val scale: Float
+ get() = dragToDesktopAnimator.animatedValue as Float
private val dragToDesktopAnimator: ValueAnimator = ValueAnimator.ofFloat(1f,
DRAG_FREEFORM_SCALE)
.setDuration(ANIMATION_DURATION.toLong())
.apply {
val t = SurfaceControl.Transaction()
val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
- addUpdateListener { animation ->
- val animatorValue = animation.animatedValue as Float
- t.setScale(taskSurface, animatorValue, animatorValue)
+ addUpdateListener {
+ t.setScale(taskSurface, scale, scale)
.setCornerRadius(taskSurface, cornerRadius)
.apply()
}
@@ -90,9 +91,9 @@
}
/**
- * Ends the animation, setting the scale and position to the final animation value
+ * Cancels the animation, intended to be used when another animator will take over.
*/
- fun endAnimator() {
- dragToDesktopAnimator.end()
+ fun cancelAnimator() {
+ dragToDesktopAnimator.cancel()
}
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
index f5a8655..b3eb2bf 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
@@ -38,6 +38,8 @@
<!-- Increase trace size: 20mb for WM and 80mb for SF -->
<option name="run-command" value="cmd window tracing size 20480"/>
<option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/>
+ <!-- uninstall Maps, so that latest version can be installed from pStash directly -->
+ <option name="run-command" value="su root pm uninstall -k --user 0 com.google.android.apps.maps"/>
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
@@ -69,6 +71,7 @@
<option name="install-arg" value="-g"/>
<option name="install-arg" value="-r"/>
<option name="test-file-name" value="pstash://com.netflix.mediaclient"/>
+ <option name="test-file-name" value="pstash://com.google.android.apps.maps"/>
</target_preparer>
<!-- Enable mocking GPS location by the test app -->
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 9f3a4d9..0c45d52 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
import android.view.Display.INVALID_DISPLAY
@@ -406,6 +407,31 @@
assertThat(listener.stashedOnSecondaryDisplay).isTrue()
}
+ @Test
+ fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
+ val taskId = 1
+ repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
+ repo.removeFreeformTask(taskId)
+ assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull()
+ }
+
+ @Test
+ fun saveBoundsBeforeMaximize_boundsSavedByTaskId() {
+ val taskId = 1
+ val bounds = Rect(0, 0, 200, 200)
+ repo.saveBoundsBeforeMaximize(taskId, bounds)
+ assertThat(repo.removeBoundsBeforeMaximize(taskId)).isEqualTo(bounds)
+ }
+
+ @Test
+ fun removeBoundsBeforeMaximize_returnsNullAfterBoundsRemoved() {
+ val taskId = 1
+ val bounds = Rect(0, 0, 200, 200)
+ repo.saveBoundsBeforeMaximize(taskId, bounds)
+ repo.removeBoundsBeforeMaximize(taskId)
+ assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull()
+ }
+
class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 4fbf2bd..93a967e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -95,9 +95,10 @@
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
-import org.mockito.kotlin.times
-import org.mockito.Mockito.`when` as whenever
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.capture
import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
/**
* Test class for {@link DesktopTasksController}
@@ -116,13 +117,14 @@
@Mock lateinit var shellCommandHandler: ShellCommandHandler
@Mock lateinit var shellController: ShellController
@Mock lateinit var displayController: DisplayController
+ @Mock lateinit var displayLayout: DisplayLayout
@Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
@Mock lateinit var syncQueue: SyncTransactionQueue
@Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock lateinit var transitions: Transitions
@Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
@Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
- @Mock lateinit var mToggleResizeDesktopTaskTransitionHandler:
+ @Mock lateinit var toggleResizeDesktopTaskTransitionHandler:
ToggleResizeDesktopTaskTransitionHandler
@Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
@Mock lateinit var launchAdjacentController: LaunchAdjacentController
@@ -154,6 +156,10 @@
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+ whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+ }
controller = createController()
controller.setSplitScreenController(splitScreenController)
@@ -179,7 +185,7 @@
transitions,
enterDesktopTransitionHandler,
exitDesktopTransitionHandler,
- mToggleResizeDesktopTaskTransitionHandler,
+ toggleResizeDesktopTaskTransitionHandler,
dragToDesktopTransitionHandler,
desktopModeTaskRepository,
desktopModeLoggerTransitionObserver,
@@ -929,15 +935,74 @@
controller.enterSplit(DEFAULT_DISPLAY, false)
verify(splitScreenController).requestEnterSplitSelect(
- task2,
- any(),
- SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
- task2.configuration.windowConfiguration.bounds
+ task2,
+ any(),
+ SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ task2.configuration.windowConfiguration.bounds
)
}
- private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
- val task = createFreeformTask(displayId)
+ @Test
+ fun toggleBounds_togglesToStableBounds() {
+ val bounds = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+ controller.toggleDesktopTaskSize(task)
+ // Assert bounds set to stable bounds
+ val wct = getLatestToggleResizeDesktopTaskWct()
+ assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds)
+ .isEqualTo(STABLE_BOUNDS)
+ }
+
+ @Test
+ fun toggleBounds_lastBoundsBeforeMaximizeSaved() {
+ val bounds = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+ controller.toggleDesktopTaskSize(task)
+ assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId))
+ .isEqualTo(bounds)
+ }
+
+ @Test
+ fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() {
+ val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+ // Maximize
+ controller.toggleDesktopTaskSize(task)
+ task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+ // Restore
+ controller.toggleDesktopTaskSize(task)
+
+ // Assert bounds set to last bounds before maximize
+ val wct = getLatestToggleResizeDesktopTaskWct()
+ assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds)
+ .isEqualTo(boundsBeforeMaximize)
+ }
+
+ @Test
+ fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() {
+ val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+ // Maximize
+ controller.toggleDesktopTaskSize(task)
+ task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+ // Restore
+ controller.toggleDesktopTaskSize(task)
+
+ // Assert last bounds before maximize removed after use
+ assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
+ }
+
+ private fun setUpFreeformTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ bounds: Rect? = null
+ ): RunningTaskInfo {
+ val task = createFreeformTask(displayId, bounds)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
desktopModeTaskRepository.addOrMoveFreeformTaskToTop(task.taskId)
@@ -1004,6 +1069,18 @@
return arg.value
}
+ private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction {
+ val arg: ArgumentCaptor<WindowContainerTransaction> =
+ ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
+ .startTransition(capture(arg))
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(capture(arg))
+ }
+ return arg.value
+ }
+
private fun getLatestMoveToDesktopWct(): WindowContainerTransaction {
val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
if (ENABLE_SHELL_TRANSITIONS) {
@@ -1042,6 +1119,7 @@
companion object {
const val SECOND_DISPLAY = 2
+ private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index 2f6f320..52da7fb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -22,6 +22,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.graphics.Rect
import android.view.Display.DEFAULT_DISPLAY
import com.android.wm.shell.MockToken
import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -31,13 +32,17 @@
/** Create a task that has windowing mode set to [WINDOWING_MODE_FREEFORM] */
@JvmStatic
@JvmOverloads
- fun createFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ fun createFreeformTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ bounds: Rect? = null
+ ): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.setLastActiveTime(100)
+ .apply { bounds?.let { setBounds(it) }}
.build()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 98e90d6..2ade3fb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -190,7 +190,7 @@
handler.cancelDragToDesktopTransition()
// Cancel animation should run since it had already started.
- verify(dragAnimator).endAnimator()
+ verify(dragAnimator).cancelAnimator()
}
@Test
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
index e43b09e..f65a1b7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
@@ -20,7 +20,9 @@
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -69,6 +71,7 @@
},
scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = .32f),
shape = EntryShape.TopRoundedCorner,
+ windowInsets = WindowInsets.navigationBars,
dragHandle = null,
// Never take over the full screen. We always want to leave some top scrim space
// for exiting and viewing the underlying app to help a user gain context.
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 38a3a2a..a33c160 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -258,6 +258,7 @@
Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED,
Settings.Secure.HUB_MODE_TUTORIAL_STATE,
+ Settings.Secure.GLANCEABLE_HUB_ENABLED,
Settings.Secure.STYLUS_BUTTONS_ENABLED,
Settings.Secure.STYLUS_HANDWRITING_ENABLED,
Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 252cb8f..1bff592 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -416,6 +416,7 @@
BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.DND_CONFIGS_MIGRATED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.HUB_MODE_TUTORIAL_STATE, NON_NEGATIVE_INTEGER_VALIDATOR);
+ VALIDATORS.put(Secure.GLANCEABLE_HUB_ENABLED, new InclusiveIntegerRangeValidator(0, 1));
VALIDATORS.put(Secure.STYLUS_BUTTONS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.STYLUS_HANDWRITING_ENABLED,
new DiscreteValueValidator(new String[] {"-1", "0", "1"}));
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 74d61ca..089782c 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -249,6 +249,9 @@
resource_dirs: [
"tests/res",
],
+ asset_dirs: [
+ "tests/goldens",
+ ],
static_libs: [
"SystemUI-res",
"WifiTrackerLib",
@@ -349,6 +352,8 @@
"androidx.test.ext.junit",
"androidx.test.ext.truth",
"kotlin-test",
+ "platform-screenshot-diff-core",
+ "PlatformMotionTesting",
"SystemUICustomizationTestUtils",
"androidx.compose.runtime_runtime",
"kosmos",
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index a6d750f..1ce3be8 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -48,6 +48,7 @@
"SystemUIShaderLib",
"WindowManager-Shell-shared",
"animationlib",
+ "com_android_systemui_shared_flags_lib",
],
manifest: "AndroidManifest.xml",
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 ea1cb34..9ce30fd 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -790,7 +790,7 @@
controller,
endState,
windowBackgroundColor,
- fadeOutWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
+ fadeWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
drawHole = !controller.isBelowAnimatingWindow,
)
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index 24cc8a4..b89ebfc 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -916,6 +916,12 @@
endController.transitionContainer = value
}
+ // We tell TransitionController that this is always a launch, and handle the launch
+ // vs return logic internally.
+ // TODO(b/323863002): maybe move the launch vs return logic out of this class and
+ // delegate it to TransitionController?
+ override val isLaunching: Boolean = true
+
override fun createAnimatorState(): TransitionAnimator.State {
return startController.createAnimatorState()
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
index 3f57f88..9ad0fc5 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
@@ -64,6 +64,7 @@
private var interactionJankMonitor: InteractionJankMonitor =
InteractionJankMonitor.getInstance(),
) : ActivityTransitionAnimator.Controller {
+ override val isLaunching: Boolean = true
/** The container to which we will add the ghost view and expanding background. */
override var transitionContainer = ghostedView.rootView as ViewGroup
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 5e4276c..9bf6b34 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -28,7 +28,9 @@
import android.view.View
import android.view.ViewGroup
import android.view.animation.Interpolator
+import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators.LINEAR
+import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
import kotlin.math.roundToInt
private const val TAG = "TransitionAnimator"
@@ -70,13 +72,14 @@
interface Controller {
/**
* The container in which the view that started the animation will be animating together
- * with the opening window.
+ * with the opening or closing window.
*
* This will be used to:
* - Get the associated [Context].
- * - Compute whether we are expanding fully above the transition container.
- * - Get to overlay to which we initially put the window background layer, until the opening
- * window is made visible (see [openingWindowSyncView]).
+ * - Compute whether we are expanding to or contracting from fully above the transition
+ * container.
+ * - Get the overlay into which we put the window background layer, while the animating
+ * window is not visible (see [openingWindowSyncView]).
*
* This container can be changed to force this [Controller] to animate the expanding view
* inside a different location, for instance to ensure correct layering during the
@@ -84,12 +87,17 @@
*/
var transitionContainer: ViewGroup
+ /** Whether the animation being controlled is a launch or a return. */
+ val isLaunching: Boolean
+
/**
- * The [View] with which the opening app window should be synchronized with once it starts
- * to be visible.
+ * If [isLaunching], the [View] with which the opening app window should be synchronized
+ * once it starts to be visible. Otherwise, the [View] with which the closing app window
+ * should be synchronized until it stops being visible.
*
* We will also move the window background layer to this view's overlay once the opening
- * window is visible.
+ * window is visible (if [isLaunching]), or from this view's overlay once the closing window
+ * stop being visible (if ![isLaunching]).
*
* If null, this will default to [transitionContainer].
*/
@@ -203,17 +211,56 @@
* layer with [windowBackgroundColor] will fade in then (optionally) fade out above the
* expanding view, and should be the same background color as the opening (or closing) window.
*
- * If [fadeOutWindowBackgroundLayer] is true, then this intermediary layer will fade out during
- * the second half of the animation, and will have SRC blending mode (ultimately punching a hole
- * in the [transition container][Controller.transitionContainer]) iff [drawHole] is true.
+ * If [fadeWindowBackgroundLayer] is true, then this intermediary layer will fade out during the
+ * second half of the animation (if [Controller.isLaunching] or fade in during the first half of
+ * 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.
*/
fun startAnimation(
controller: Controller,
endState: State,
windowBackgroundColor: Int,
- fadeOutWindowBackgroundLayer: Boolean = true,
+ fadeWindowBackgroundLayer: Boolean = true,
drawHole: Boolean = false,
): Animation {
+ if (!controller.isLaunching) 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
+ // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
+ // transition container and reveal the opening window.
+ val windowBackgroundLayer =
+ GradientDrawable().apply {
+ setColor(windowBackgroundColor)
+ alpha = 0
+ }
+
+ val animator =
+ createAnimator(
+ controller,
+ endState,
+ windowBackgroundLayer,
+ fadeWindowBackgroundLayer,
+ drawHole
+ )
+ animator.start()
+
+ return object : Animation {
+ override fun cancel() {
+ animator.cancel()
+ }
+ }
+ }
+
+ @VisibleForTesting
+ fun createAnimator(
+ controller: Controller,
+ endState: State,
+ windowBackgroundLayer: GradientDrawable,
+ fadeWindowBackgroundLayer: Boolean = true,
+ drawHole: Boolean = false
+ ): ValueAnimator {
val state = controller.createAnimatorState()
// Start state.
@@ -255,31 +302,24 @@
val transitionContainer = controller.transitionContainer
val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
- // 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
- // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
- // transition container and reveal the opening window.
- val windowBackgroundLayer =
- GradientDrawable().apply {
- setColor(windowBackgroundColor)
- alpha = 0
- }
-
// Update state.
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = timings.totalDuration
animator.interpolator = LINEAR
// Whether we should move the [windowBackgroundLayer] into the overlay of
- // [Controller.openingWindowSyncView] once the opening app window starts to be visible.
+ // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or
+ // from it once the closing app window stops being visible.
+ // This is necessary as a one-off sync so we can avoid syncing at every frame, especially
+ // in complex interactions like launching an activity from a dialog. See
+ // b/214961273#comment2 for more details.
val openingWindowSyncView = controller.openingWindowSyncView
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
- val moveBackgroundLayerWhenAppIsVisible =
+ val moveBackgroundLayerWhenAppVisibilityChanges =
openingWindowSyncView != null &&
openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
val transitionContainerOverlay = transitionContainer.overlay
- var cancelled = false
var movedBackgroundLayer = false
animator.addListener(
@@ -293,7 +333,11 @@
// Add the drawable to the transition container overlay. Overlays always draw
// drawables after views, so we know that it will be drawn above any view added
// by the controller.
- transitionContainerOverlay.add(windowBackgroundLayer)
+ if (controller.isLaunching || openingWindowSyncViewOverlay == null) {
+ transitionContainerOverlay.add(windowBackgroundLayer)
+ } else {
+ openingWindowSyncViewOverlay.add(windowBackgroundLayer)
+ }
}
override fun onAnimationEnd(animation: Animator) {
@@ -303,7 +347,7 @@
controller.onTransitionAnimationEnd(isExpandingFullyAbove)
transitionContainerOverlay.remove(windowBackgroundLayer)
- if (moveBackgroundLayerWhenAppIsVisible) {
+ if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
}
}
@@ -311,12 +355,6 @@
)
animator.addUpdateListener { animation ->
- if (cancelled) {
- // TODO(b/184121838): Cancel the animator directly instead of just skipping the
- // update.
- return@addUpdateListener
- }
-
maybeUpdateEndState()
// TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non
@@ -338,20 +376,34 @@
state.bottomCornerRadius =
MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
- // The expanding view can/should be hidden once it is completely covered by the opening
- // window.
state.visible =
- getProgress(
- timings,
- linearProgress,
- timings.contentBeforeFadeOutDelay,
- timings.contentBeforeFadeOutDuration
- ) < 1
+ if (controller.isLaunching) {
+ // The expanding view can/should be hidden once it is completely covered by the
+ // opening window.
+ getProgress(
+ timings,
+ linearProgress,
+ timings.contentBeforeFadeOutDelay,
+ timings.contentBeforeFadeOutDuration
+ ) < 1
+ } else {
+ getProgress(
+ timings,
+ linearProgress,
+ timings.contentAfterFadeInDelay,
+ timings.contentAfterFadeInDuration
+ ) > 0
+ }
- if (moveBackgroundLayerWhenAppIsVisible && !state.visible && !movedBackgroundLayer) {
- // The expanding view is not visible, so the opening app is visible. If this is the
- // first frame when it happens, trigger a one-off sync and move the background layer
- // in its new container.
+ if (
+ controller.isLaunching &&
+ moveBackgroundLayerWhenAppVisibilityChanges &&
+ !state.visible &&
+ !movedBackgroundLayer
+ ) {
+ // The expanding view is not visible, so the opening app is visible. If this is
+ // the first frame when it happens, trigger a one-off sync and move the
+ // background layer in its new container.
movedBackgroundLayer = true
transitionContainerOverlay.remove(windowBackgroundLayer)
@@ -362,6 +414,25 @@
openingWindowSyncView,
then = {}
)
+ } else if (
+ !controller.isLaunching &&
+ moveBackgroundLayerWhenAppVisibilityChanges &&
+ state.visible &&
+ !movedBackgroundLayer
+ ) {
+ // The contracting view is now visible, so the closing app is not. If this is
+ // the first frame when it happens, trigger a one-off sync and move the
+ // background layer in its new container.
+ movedBackgroundLayer = true
+
+ openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
+ transitionContainerOverlay.add(windowBackgroundLayer)
+
+ ViewRootSync.synchronizeNextDraw(
+ openingWindowSyncView,
+ transitionContainer,
+ then = {}
+ )
}
val container =
@@ -376,19 +447,14 @@
state,
linearProgress,
container,
- fadeOutWindowBackgroundLayer,
- drawHole
+ fadeWindowBackgroundLayer,
+ drawHole,
+ controller.isLaunching
)
controller.onTransitionAnimationProgress(state, progress, linearProgress)
}
- animator.start()
- return object : Animation {
- override fun cancel() {
- cancelled = true
- animator.cancel()
- }
- }
+ return animator
}
/** Return whether we are expanding fully above the [transitionContainer]. */
@@ -405,8 +471,9 @@
state: State,
linearProgress: Float,
transitionContainer: View,
- fadeOutWindowBackgroundLayer: Boolean,
- drawHole: Boolean
+ fadeWindowBackgroundLayer: Boolean,
+ drawHole: Boolean,
+ isLaunching: Boolean
) {
// Update position.
transitionContainer.getLocationOnScreen(transitionContainerLocation)
@@ -437,27 +504,64 @@
timings.contentBeforeFadeOutDelay,
timings.contentBeforeFadeOutDuration
)
- if (fadeInProgress < 1) {
- val alpha =
- interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
- drawable.alpha = (alpha * 0xFF).roundToInt()
- } else if (fadeOutWindowBackgroundLayer) {
- val fadeOutProgress =
- getProgress(
- timings,
- linearProgress,
- timings.contentAfterFadeInDelay,
- timings.contentAfterFadeInDuration
- )
- val alpha =
- 1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress)
- drawable.alpha = (alpha * 0xFF).roundToInt()
- if (drawHole) {
- drawable.setXfermode(SRC_MODE)
+ if (isLaunching) {
+ if (fadeInProgress < 1) {
+ val alpha =
+ 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(
+ fadeOutProgress
+ )
+ drawable.alpha = (alpha * 0xFF).roundToInt()
+
+ if (drawHole) {
+ drawable.setXfermode(SRC_MODE)
+ }
+ } else {
+ drawable.alpha = 0xFF
}
} else {
- drawable.alpha = 0xFF
+ if (fadeInProgress < 1 && fadeWindowBackgroundLayer) {
+ val alpha =
+ interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
+ drawable.alpha = (alpha * 0xFF).roundToInt()
+
+ if (drawHole) {
+ drawable.setXfermode(SRC_MODE)
+ }
+ } else {
+ val fadeOutProgress =
+ getProgress(
+ timings,
+ linearProgress,
+ timings.contentAfterFadeInDelay,
+ timings.contentAfterFadeInDuration
+ )
+ val alpha =
+ 1 -
+ interpolators.contentAfterFadeInInterpolator.getInterpolation(
+ fadeOutProgress
+ )
+ drawable.alpha = (alpha * 0xFF).roundToInt()
+ drawable.setXfermode(null)
+ }
+ }
+ }
+
+ private fun checkReturnAnimationFrameworkFlag() {
+ check(returnAnimationFrameworkLibrary()) {
+ "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is disabled"
}
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
index 974ee3a..c7f0a96 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
@@ -168,6 +168,9 @@
override var transitionContainer: ViewGroup = composeViewRoot.rootView as ViewGroup
+ // TODO(b/323863002): update to be dependant on usage.
+ override val isLaunching: Boolean = true
+
override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
animatorState.value = null
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
index 6b28319..f71121c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
@@ -25,13 +25,13 @@
import android.content.pm.UserInfo
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.communal.data.model.DisabledReason
-import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_ENABLED
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -95,15 +95,27 @@
@Test
fun hubIsDisabledByUser() =
testScope.runTest {
- kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
+ kosmos.fakeSettings.putIntForUser(
+ Settings.Secure.GLANCEABLE_HUB_ENABLED,
+ 0,
+ PRIMARY_USER.id
+ )
val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
assertThat(enabledState?.enabled).isFalse()
assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING)
- kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id)
+ kosmos.fakeSettings.putIntForUser(
+ Settings.Secure.GLANCEABLE_HUB_ENABLED,
+ 1,
+ SECONDARY_USER.id
+ )
assertThat(enabledState?.enabled).isFalse()
- kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id)
+ kosmos.fakeSettings.putIntForUser(
+ Settings.Secure.GLANCEABLE_HUB_ENABLED,
+ 1,
+ PRIMARY_USER.id
+ )
assertThat(enabledState?.enabled).isTrue()
}
@@ -126,7 +138,11 @@
val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
assertThat(enabledState?.enabled).isTrue()
- kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
+ kosmos.fakeSettings.putIntForUser(
+ Settings.Secure.GLANCEABLE_HUB_ENABLED,
+ 0,
+ PRIMARY_USER.id
+ )
setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL)
assertThat(enabledState?.enabled).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 2c9d72c..6cae5d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -19,11 +19,11 @@
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
import android.os.UserHandle
+import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_ENABLED
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
@@ -220,7 +220,11 @@
fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
fakeKeyguardRepository.setKeyguardShowing(true)
val settingsValue = if (available) 1 else 0
- fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, settingsValue, MAIN_USER_INFO.id)
+ fakeSettings.putIntForUser(
+ Settings.Secure.GLANCEABLE_HUB_ENABLED,
+ settingsValue,
+ MAIN_USER_INFO.id
+ )
}
private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaRecommendationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaRecommendationsInteractorTest.kt
index 28995e1..9656511 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaRecommendationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaRecommendationsInteractorTest.kt
@@ -17,10 +17,16 @@
package com.android.systemui.media.controls.domain.interactor
import android.R
+import android.content.ComponentName
+import android.content.Intent
+import android.content.applicationContext
import android.graphics.drawable.Icon
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Expandable
+import com.android.systemui.broadcast.broadcastSender
+import com.android.systemui.broadcast.mockBroadcastSender
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -28,25 +34,36 @@
import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor.Companion.EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaRecommendationsInteractor
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.shared.model.MediaRecModel
import com.android.systemui.media.controls.shared.model.MediaRecommendationsModel
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.plugins.activityStarter
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
class MediaRecommendationsInteractorTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val spyContext = spy(context)
+ private val kosmos = testKosmos().apply { applicationContext = spyContext }
private val testScope = kosmos.testScope
private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
+ private val activityStarter = kosmos.activityStarter
private val icon: Icon = Icon.createWithResource(context, R.drawable.ic_media_play)
private val smartspaceMediaData: SmartspaceMediaData =
SmartspaceMediaData(
@@ -56,7 +73,11 @@
recommendations = MediaTestHelper.getValidRecommendationList(icon),
)
- private val underTest: MediaRecommendationsInteractor = kosmos.mediaRecommendationsInteractor
+ private val underTest: MediaRecommendationsInteractor =
+ with(kosmos) {
+ broadcastSender = mockBroadcastSender
+ kosmos.mediaRecommendationsInteractor
+ }
@Test
fun addRecommendation_smartspaceMediaDataUpdate() =
@@ -111,6 +132,50 @@
assertThat(recommendations?.mediaRecs?.isEmpty()).isTrue()
}
+ @Test
+ fun removeRecommendation_noTrampolineActivity() {
+ val intent = Intent()
+
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ underTest.removeMediaRecommendations(KEY_MEDIA_SMARTSPACE, intent, 0)
+
+ verify(kosmos.mockBroadcastSender).sendBroadcast(eq(intent))
+ }
+
+ @Test
+ fun removeRecommendation_usingTrampolineActivity() {
+ doNothing().whenever(spyContext).startActivity(any())
+ val intent = Intent()
+
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.component = ComponentName(PACKAGE_NAME, EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME)
+
+ underTest.removeMediaRecommendations(KEY_MEDIA_SMARTSPACE, intent, 0)
+
+ verify(spyContext).startActivity(eq(intent))
+ }
+
+ @Test
+ fun startSettings() {
+ underTest.startSettings()
+
+ verify(activityStarter).startActivity(any(), eq(true))
+ }
+
+ @Test
+ fun startClickIntent() {
+ doNothing().whenever(spyContext).startActivity(any())
+ val intent = Intent()
+ val expandable = mock<Expandable>()
+
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ underTest.startClickIntent(expandable, intent)
+
+ verify(spyContext).startActivity(eq(intent))
+ }
+
companion object {
private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
private const val PACKAGE_NAME = "com.example.app"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelTest.kt
new file mode 100644
index 0000000..51b1911
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.R
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.graphics.drawable.Icon
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
+import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaRecommendationsViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
+ private val packageManager = kosmos.packageManager
+ private val icon: Icon = Icon.createWithResource(context, R.drawable.ic_media_play)
+ private val drawable = context.getDrawable(R.drawable.ic_media_play)
+ private val smartspaceMediaData: SmartspaceMediaData =
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ packageName = PACKAGE_NAME,
+ recommendations = MediaTestHelper.getValidRecommendationList(icon),
+ )
+
+ private val underTest: MediaRecommendationsViewModel = kosmos.mediaRecommendationsViewModel
+
+ @Test
+ fun loadRecommendations_recsCardViewModelIsLoaded() =
+ testScope.runTest {
+ whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable)
+ whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
+ .thenReturn(drawable)
+ whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt()))
+ .thenReturn(ApplicationInfo())
+ whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
+ val recsCardViewModel by collectLastValue(underTest.mediaRecsCard)
+
+ context.setMockPackageManager(packageManager)
+
+ mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, smartspaceMediaData)
+
+ assertThat(recsCardViewModel).isNotNull()
+ assertThat(recsCardViewModel?.mediaRecs?.size)
+ .isEqualTo(smartspaceMediaData.recommendations.size)
+ }
+
+ companion object {
+ private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+ private const val PACKAGE_NAME = "com.example.app"
+ }
+}
diff --git a/packages/SystemUI/res/drawable/ic_finder_active.xml b/packages/SystemUI/res/drawable/ic_finder_active.xml
index 8ca221a..2423e34 100644
--- a/packages/SystemUI/res/drawable/ic_finder_active.xml
+++ b/packages/SystemUI/res/drawable/ic_finder_active.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
@@ -10,5 +11,6 @@
<path
android:pathData="M12.797,4.005C11.949,3.936 11.203,4.597 11.203,5.467V6.659C8.855,7.001 6.998,8.856 6.653,11.203H5.467C4.597,11.203 3.936,11.948 4.005,12.796L4.006,12.802L4.006,12.809C4.38,16.605 7.399,19.625 11.195,20C12.051,20.087 12.803,19.404 12.803,18.547V17.355C15.154,17.012 17.013,15.154 17.355,12.803H18.54C19.406,12.803 20.079,12.058 19.992,11.196C19.618,7.4 16.606,4.388 12.812,4.006L12.804,4.006L12.797,4.005ZM11.203,9.344V8.283C9.741,8.591 8.588,9.741 8.278,11.203H9.344C9.585,10.4 10.179,9.754 10.942,9.437C11.027,9.402 11.114,9.371 11.203,9.344ZM11.998,13.171C11.358,13.175 10.828,12.651 10.827,12.004H10.827C10.827,11.959 10.83,11.915 10.835,11.871C10.885,11.427 11.185,11.056 11.59,10.902C11.694,10.863 11.806,10.838 11.921,10.83C11.948,10.833 11.976,10.834 12.003,10.834C12.65,10.834 13.177,11.356 13.179,12.007C13.177,12.622 12.695,13.13 12.091,13.175C12.06,13.172 12.029,13.17 11.998,13.171ZM17.353,11.203H18.383C18.028,8.289 15.72,5.979 12.804,5.616V6.658C15.153,7 17.004,8.852 17.353,11.203ZM14.663,11.203C14.395,10.311 13.692,9.611 12.804,9.344V8.283C14.265,8.59 15.414,9.736 15.727,11.203H14.663ZM5.615,12.803H6.654C7.001,15.15 8.855,17.002 11.203,17.346V18.391C8.287,18.034 5.972,15.719 5.615,12.803ZM11.203,14.666C10.316,14.394 9.613,13.692 9.345,12.803H8.279C8.591,14.264 9.741,15.412 11.203,15.721V14.666ZM14.661,12.811H15.729C15.418,14.272 14.266,15.422 12.804,15.73V14.662C13.689,14.396 14.391,13.699 14.661,12.811Z"
android:fillColor="#ffffff"
- android:fillType="evenOdd"/>
+ android:fillType="evenOdd"
+ tools:ignore="VectorPath" />
</vector>
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
index 7c65d21..c724244 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -21,6 +21,7 @@
import android.appwidget.AppWidgetProviderInfo
import android.content.IntentFilter
import android.content.pm.UserInfo
+import android.provider.Settings
import com.android.systemui.Flags.communalHub
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.communal.data.model.CommunalEnabledState
@@ -116,12 +117,12 @@
private fun getEnabledByUser(user: UserInfo): Flow<Boolean> =
secureSettings
- .observerFlow(userId = user.id, names = arrayOf(GLANCEABLE_HUB_ENABLED))
+ .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.GLANCEABLE_HUB_ENABLED))
// Force an update
.onStart { emit(Unit) }
.map {
secureSettings.getIntForUser(
- GLANCEABLE_HUB_ENABLED,
+ Settings.Secure.GLANCEABLE_HUB_ENABLED,
ENABLED_SETTING_DEFAULT,
user.id,
) == 1
@@ -138,7 +139,6 @@
.map { devicePolicyManager.areKeyguardWidgetsAllowed(user.id) }
companion object {
- const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
const val GLANCEABLE_HUB_CONTENT_SETTING = "glanceable_hub_content_setting"
private const val ENABLED_SETTING_DEFAULT = 1
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 53aee5d..fb0d225 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -178,8 +178,6 @@
import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
import com.android.wm.shell.keyguard.KeyguardTransitions;
-import dagger.Lazy;
-
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -189,6 +187,7 @@
import java.util.concurrent.Executor;
import java.util.function.Consumer;
+import dagger.Lazy;
import kotlinx.coroutines.CoroutineDispatcher;
/**
@@ -964,6 +963,13 @@
@VisibleForTesting
final ActivityTransitionAnimator.Controller mOccludeAnimationController =
new ActivityTransitionAnimator.Controller() {
+ private boolean mIsLaunching = true;
+
+ @Override
+ public boolean isLaunching() {
+ return mIsLaunching;
+ }
+
@Override
public void onTransitionAnimationStart(boolean isExpandingFullyAbove) {
mOccludeAnimationPlaying = true;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt
index aab90c3..585bd6a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt
@@ -263,6 +263,7 @@
@VisibleForTesting
val occludeAnimationController: ActivityTransitionAnimator.Controller =
object : ActivityTransitionAnimator.Controller {
+ override val isLaunching: Boolean = true
override var transitionContainer: ViewGroup
get() = keyguardViewController.get().getViewRootImpl().view as ViewGroup
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 8682dd3..4bf5200 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -24,13 +24,10 @@
import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
-import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
-import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.util.kotlin.Utils.Companion.sample
-import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineDispatcher
@@ -72,79 +69,73 @@
* Listen for the signal that we're waking up and figure what state we need to transition to.
*/
private fun listenForAodToAwake() {
- val transitionToLockscreen: suspend (TransitionStep) -> UUID? =
- { startedStep: TransitionStep ->
- val modeOnCanceled =
- if (startedStep.from == KeyguardState.LOCKSCREEN) {
- TransitionModeOnCanceled.REVERSE
- } else if (startedStep.from == KeyguardState.GONE) {
- TransitionModeOnCanceled.RESET
- } else {
- TransitionModeOnCanceled.LAST_VALUE
- }
- startTransitionTo(
- toState = KeyguardState.LOCKSCREEN,
- modeOnCanceled = modeOnCanceled,
+ // Use PowerInteractor's wakefulness, which is the earliest wake signal available. We
+ // have all of the information we need at this time to make a decision about where to
+ // transition.
+ scope.launch {
+ powerInteractor.detailedWakefulness
+ .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() }
+ .sample(
+ startedKeyguardTransitionStep,
+ keyguardInteractor.biometricUnlockState,
+ keyguardInteractor.primaryBouncerShowing,
+ keyguardInteractor.isKeyguardShowing,
+ keyguardInteractor.isKeyguardOccluded,
+ keyguardInteractor.isKeyguardDismissible,
)
- }
+ .collect {
+ (
+ _,
+ startedStep,
+ biometricUnlockState,
+ primaryBouncerShowing,
+ _,
+ isKeyguardOccludedLegacy,
+ _) ->
+ if (!maybeHandleInsecurePowerGesture()) {
+ val shouldTransitionToLockscreen =
+ if (KeyguardWmStateRefactor.isEnabled) {
+ // Check with the superclass to see if an occlusion transition is
+ // needed. Also, don't react to wake and unlock events, as we'll be
+ // receiving a call to #dismissAod() shortly when the authentication
+ // completes.
+ !maybeStartTransitionToOccludedOrInsecureCamera() &&
+ !isWakeAndUnlock(biometricUnlockState) &&
+ !primaryBouncerShowing
+ } else {
+ !isKeyguardOccludedLegacy &&
+ !isWakeAndUnlock(biometricUnlockState) &&
+ !primaryBouncerShowing
+ }
- if (KeyguardWmStateRefactor.isEnabled) {
- // The refactor uses PowerInteractor's wakefulness, which is the earliest wake signal
- // available. We have all of the information we need at this time to make a decision
- // about where to transition.
- scope.launch {
- powerInteractor.detailedWakefulness
- // React only to wake events.
- .filterRelevantKeyguardStateAnd { it.isAwake() }
- .sample(
- startedKeyguardTransitionStep,
- keyguardInteractor.biometricUnlockState,
- keyguardInteractor.primaryBouncerShowing,
- )
- // Make sure we've at least STARTED a transition to AOD.
- .collect { (_, startedStep, biometricUnlockState, primaryBouncerShowing) ->
- // Check with the superclass to see if an occlusion transition is needed.
- // Also, don't react to wake and unlock events, as we'll be receiving a call
- // to #dismissAod() shortly when the authentication completes.
- if (
- !maybeStartTransitionToOccludedOrInsecureCamera() &&
- !isWakeAndUnlock(biometricUnlockState) &&
- !primaryBouncerShowing
- ) {
- transitionToLockscreen(startedStep)
+ // With the refactor enabled, maybeStartTransitionToOccludedOrInsecureCamera
+ // handles transitioning to OCCLUDED.
+ val shouldTransitionToOccluded =
+ !KeyguardWmStateRefactor.isEnabled && isKeyguardOccludedLegacy
+
+ if (shouldTransitionToLockscreen) {
+ val modeOnCanceled =
+ if (startedStep.from == KeyguardState.LOCKSCREEN) {
+ TransitionModeOnCanceled.REVERSE
+ } else if (startedStep.from == KeyguardState.GONE) {
+ TransitionModeOnCanceled.RESET
+ } else {
+ TransitionModeOnCanceled.LAST_VALUE
+ }
+
+ startTransitionTo(
+ toState = KeyguardState.LOCKSCREEN,
+ modeOnCanceled = modeOnCanceled,
+ ownerReason = "listen for aod to awake"
+ )
+ } else if (shouldTransitionToOccluded) {
+ startTransitionTo(
+ toState = KeyguardState.OCCLUDED,
+ ownerReason = "waking up and isOccluded=true",
+ )
}
}
- }
- } else {
- scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.FINISH)
- .filterRelevantKeyguardState()
- .sample(
- keyguardInteractor.isKeyguardShowing,
- startedKeyguardTransitionStep,
- keyguardInteractor.isKeyguardOccluded,
- keyguardInteractor.biometricUnlockState,
- keyguardInteractor.primaryBouncerShowing,
- )
- .collect {
- (
- _,
- isKeyguardShowing,
- lastStartedStep,
- occluded,
- biometricUnlockState,
- primaryBouncerShowing) ->
- if (
- !occluded &&
- !isWakeAndUnlock(biometricUnlockState) &&
- isKeyguardShowing &&
- !primaryBouncerShowing
- ) {
- transitionToLockscreen(lastStartedStep)
- }
- }
- }
+ }
}
}
@@ -165,7 +156,8 @@
.collect {
startTransitionTo(
toState = KeyguardState.OCCLUDED,
- modeOnCanceled = TransitionModeOnCanceled.RESET
+ modeOnCanceled = TransitionModeOnCanceled.RESET,
+ ownerReason = "isOccluded = true",
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 599285e..e456a55 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -120,25 +120,15 @@
* Returns true if a transition was started, false otherwise.
*/
suspend fun maybeStartTransitionToOccludedOrInsecureCamera(): Boolean {
+ // The refactor is required for the occlusion interactor to work.
+ KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode()
+
+ // Check if we should start a transition from the power gesture.
if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) {
- if (transitionInteractor.getCurrentState() == KeyguardState.GONE) {
- // If the current state is GONE when the launch gesture is triggered, it means we
- // were in transition from GONE -> DOZING/AOD due to the first power button tap. The
- // second tap indicates that the user's intent was actually to launch the unlocked
- // (insecure) camera, so we should transition back to GONE.
- startTransitionTo(
- KeyguardState.GONE,
- ownerReason = "Power button gesture while GONE"
- )
- } else if (keyguardOcclusionInteractor.occludingActivityWillDismissKeyguard.value) {
- // The double tap gesture occurred while not GONE (AOD/LOCKSCREEN/etc.), but the
- // keyguard is dismissable. The activity launch will dismiss the keyguard, so we
- // should transition to GONE.
- startTransitionTo(
- KeyguardState.GONE,
- ownerReason = "Power button gesture on dismissable keyguard"
- )
- } else {
+ // See if we handled the insecure power gesture. If not, then we'll be launching the
+ // secure camera. Once KeyguardWmStateRefactor is fully enabled, we can clean up this
+ // code path by pulling maybeHandleInsecurePowerGesture() into this conditional.
+ if (!maybeHandleInsecurePowerGesture()) {
// Otherwise, the double tap gesture occurred while not GONE and not dismissable,
// which means we will launch the secure camera, which OCCLUDES the keyguard.
startTransitionTo(
@@ -165,6 +155,43 @@
}
/**
+ * Transition to [KeyguardState.GONE] for the insecure power button launch gesture, if the
+ * conditions to do so are met.
+ *
+ * Called from [FromAodTransitionInteractor] if [KeyguardWmStateRefactor] is not enabled, or
+ * [maybeStartTransitionToOccludedOrInsecureCamera] if it's enabled.
+ */
+ @Deprecated("Will be merged into maybeStartTransitionToOccludedOrInsecureCamera")
+ suspend fun maybeHandleInsecurePowerGesture(): Boolean {
+ if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) {
+ if (transitionInteractor.getCurrentState() == KeyguardState.GONE) {
+ // If the current state is GONE when the launch gesture is triggered, it means we
+ // were in transition from GONE -> DOZING/AOD due to the first power button tap. The
+ // second tap indicates that the user's intent was actually to launch the unlocked
+ // (insecure) camera, so we should transition back to GONE.
+ startTransitionTo(
+ KeyguardState.GONE,
+ ownerReason = "Power button gesture while GONE"
+ )
+
+ return true
+ } else if (keyguardOcclusionInteractor.occludingActivityWillDismissKeyguard.value) {
+ // The double tap gesture occurred while not GONE (AOD/LOCKSCREEN/etc.), but the
+ // keyguard is dismissable. The activity launch will dismiss the keyguard, so we
+ // should transition to GONE.
+ startTransitionTo(
+ KeyguardState.GONE,
+ ownerReason = "Power button gesture on dismissable keyguard"
+ )
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ /**
* Transition to the appropriate state when the device goes to sleep while in [from].
*
* We could also just use [fromState], but it's more readable in the From*TransitionInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt
index 40a132a..d57b049 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt
@@ -17,6 +17,13 @@
package com.android.systemui.media.controls.domain.pipeline.interactor
import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.Expandable
+import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
@@ -24,6 +31,8 @@
import com.android.systemui.media.controls.shared.model.MediaRecModel
import com.android.systemui.media.controls.shared.model.MediaRecommendationsModel
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.plugins.ActivityStarter
+import java.net.URISyntaxException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -42,6 +51,8 @@
@Application private val applicationContext: Context,
repository: MediaFilterRepository,
private val mediaDataProcessor: MediaDataProcessor,
+ private val broadcastSender: BroadcastSender,
+ private val activityStarter: ActivityStarter,
) {
val recommendations: Flow<MediaRecommendationsModel> =
@@ -54,8 +65,53 @@
.distinctUntilChanged()
.stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
- fun removeMediaRecommendations(key: String, delayMs: Long) {
+ fun removeMediaRecommendations(key: String, dismissIntent: Intent?, delayMs: Long) {
mediaDataProcessor.dismissSmartspaceRecommendation(key, delayMs)
+ if (dismissIntent == null) {
+ Log.w(TAG, "Cannot create dismiss action click action: extras missing dismiss_intent.")
+ return
+ }
+
+ val className = dismissIntent.component?.className
+ if (className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) {
+ // Dismiss the card Smartspace data through Smartspace trampoline activity.
+ applicationContext.startActivity(dismissIntent)
+ } else {
+ broadcastSender.sendBroadcast(dismissIntent)
+ }
+ }
+
+ fun startSettings() {
+ activityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */ true)
+ }
+
+ fun startClickIntent(expandable: Expandable, intent: Intent) {
+ if (shouldActivityOpenInForeground(intent)) {
+ // Request to unlock the device if the activity needs to be opened in foreground.
+ activityStarter.postStartActivityDismissingKeyguard(
+ intent,
+ 0 /* delay */,
+ expandable.activityTransitionController(
+ InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER
+ )
+ )
+ } else {
+ // Otherwise, open the activity in background directly.
+ applicationContext.startActivity(intent)
+ }
+ }
+
+ /** Returns if the action will open the activity in foreground. */
+ private fun shouldActivityOpenInForeground(intent: Intent): Boolean {
+ val intentString = intent.extras?.getString(EXTRAS_SMARTSPACE_INTENT) ?: return false
+ try {
+ val wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME)
+ return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false)
+ } catch (e: URISyntaxException) {
+ Log.wtf(TAG, "Failed to create intent from URI: $intentString")
+ e.printStackTrace()
+ }
+ return false
}
private fun toRecommendationsModel(data: SmartspaceMediaData): MediaRecommendationsModel {
@@ -76,4 +132,21 @@
)
}
}
+
+ companion object {
+
+ private const val TAG = "MediaRecommendationsInteractor"
+
+ // TODO (b/237284176) : move AGSA reference out.
+ private const val EXTRAS_SMARTSPACE_INTENT =
+ "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT"
+ @VisibleForTesting
+ const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME =
+ "com.google.android.apps.gsa.staticplugins.opa.smartspace." +
+ "ExportedSmartspaceTrampolineActivity"
+
+ private const val KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND"
+
+ private val SETTINGS_INTENT = Intent(Settings.ACTION_MEDIA_CONTROLS_SETTINGS)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaArtworkHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaArtworkHelper.kt
new file mode 100644
index 0000000..eec43a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaArtworkHelper.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.util
+
+import android.app.WallpaperColors
+import android.content.Context
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.Icon
+import android.graphics.drawable.LayerDrawable
+import android.util.Log
+import com.android.systemui.media.controls.ui.animation.backgroundEndFromScheme
+import com.android.systemui.media.controls.ui.animation.backgroundStartFromScheme
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.util.getColorWithAlpha
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+object MediaArtworkHelper {
+
+ /**
+ * This method should be called from a background thread. WallpaperColors.fromBitmap takes a
+ * good amount of time. We do that work on the background executor to avoid stalling animations
+ * on the UI Thread.
+ */
+ suspend fun getWallpaperColor(
+ applicationContext: Context,
+ backgroundDispatcher: CoroutineDispatcher,
+ artworkIcon: Icon?,
+ tag: String,
+ ): WallpaperColors? =
+ withContext(backgroundDispatcher) {
+ return@withContext artworkIcon?.let {
+ if (it.type == Icon.TYPE_BITMAP || it.type == Icon.TYPE_ADAPTIVE_BITMAP) {
+ // Avoids extra processing if this is already a valid bitmap
+ it.bitmap.let { artworkBitmap ->
+ if (artworkBitmap.isRecycled) {
+ Log.d(tag, "Cannot load wallpaper color from a recycled bitmap")
+ null
+ } else {
+ WallpaperColors.fromBitmap(artworkBitmap)
+ }
+ }
+ } else {
+ it.loadDrawable(applicationContext)?.let { artworkDrawable ->
+ WallpaperColors.fromDrawable(artworkDrawable)
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a scaled [Drawable] of a given [Icon] centered in [width]x[height] background size.
+ */
+ fun getScaledBackground(context: Context, icon: Icon, width: Int, height: Int): Drawable? {
+ val drawable = icon.loadDrawable(context)
+ val bounds = Rect(0, 0, width, height)
+ if (bounds.width() > width || bounds.height() > height) {
+ val offsetX = (bounds.width() - width) / 2.0f
+ val offsetY = (bounds.height() - height) / 2.0f
+ bounds.offset(-offsetX.toInt(), -offsetY.toInt())
+ }
+ drawable?.bounds = bounds
+ return drawable
+ }
+
+ /** Adds [gradient] on a given [albumArt] drawable using [colorScheme]. */
+ fun setUpGradientColorOnDrawable(
+ albumArt: Drawable?,
+ gradient: GradientDrawable,
+ colorScheme: ColorScheme,
+ startAlpha: Float,
+ endAlpha: Float
+ ): LayerDrawable {
+ gradient.colors =
+ intArrayOf(
+ getColorWithAlpha(backgroundStartFromScheme(colorScheme), startAlpha),
+ getColorWithAlpha(backgroundEndFromScheme(colorScheme), endAlpha)
+ )
+ return LayerDrawable(arrayOf(albumArt, gradient))
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/GutsViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/GutsViewModel.kt
new file mode 100644
index 0000000..e508e1b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/GutsViewModel.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.annotation.ColorInt
+import android.graphics.drawable.Drawable
+
+/** Models UI state for media guts menu */
+data class GutsViewModel(
+ val gutsText: CharSequence,
+ @ColorInt val textColor: Int,
+ @ColorInt val buttonBackgroundColor: Int,
+ @ColorInt val buttonTextColor: Int,
+ val isDismissEnabled: Boolean = true,
+ val onDismissClicked: () -> Unit,
+ val cancelTextBackground: Drawable?,
+ val onSettingsClicked: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecViewModel.kt
new file mode 100644
index 0000000..2f9fc9b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.annotation.ColorInt
+import android.graphics.drawable.Drawable
+import com.android.systemui.animation.Expandable
+
+/** Models UI state for media recommendation item */
+data class MediaRecViewModel(
+ val contentDescription: CharSequence,
+ val title: CharSequence = "",
+ @ColorInt val titleColor: Int,
+ val subtitle: CharSequence = "",
+ @ColorInt val subtitleColor: Int,
+ /** track progress [0 - 100] for the recommendation album. */
+ val progress: Int = 0,
+ @ColorInt val progressColor: Int,
+ val albumIcon: Drawable? = null,
+ val appIcon: Drawable? = null,
+ val onClicked: ((Expandable, Int) -> Unit),
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt
new file mode 100644
index 0000000..19ea00d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.app.WallpaperColors
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.Icon
+import android.graphics.drawable.LayerDrawable
+import android.os.Process
+import android.util.Log
+import androidx.appcompat.content.res.AppCompatResources
+import com.android.internal.logging.InstanceId
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor
+import com.android.systemui.media.controls.shared.model.MediaRecModel
+import com.android.systemui.media.controls.shared.model.MediaRecommendationsModel
+import com.android.systemui.media.controls.ui.animation.accentPrimaryFromScheme
+import com.android.systemui.media.controls.ui.animation.surfaceFromScheme
+import com.android.systemui.media.controls.ui.animation.textPrimaryFromScheme
+import com.android.systemui.media.controls.ui.animation.textSecondaryFromScheme
+import com.android.systemui.media.controls.ui.controller.MediaViewController.Companion.GUTS_ANIMATION_DURATION
+import com.android.systemui.media.controls.ui.util.MediaArtworkHelper
+import com.android.systemui.media.controls.util.MediaDataUtils
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.monet.Style
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/** Models UI state and handles user input for media recommendations */
+@SysUISingleton
+class MediaRecommendationsViewModel
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val interactor: MediaRecommendationsInteractor,
+ private val logger: MediaUiEventLogger,
+) {
+
+ val mediaRecsCard: Flow<MediaRecsCardViewModel?> =
+ interactor.recommendations
+ .map { recsCard -> toRecsViewModel(recsCard) }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ /**
+ * Called whenever the recommendation has been expired or removed by the user. This method
+ * removes the recommendation card entirely from the carousel.
+ */
+ private fun onMediaRecommendationsDismissed(
+ key: String,
+ uid: Int,
+ packageName: String,
+ dismissIntent: Intent?,
+ instanceId: InstanceId?
+ ) {
+ // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_DISMISS_EVENT).
+ logger.logLongPressDismiss(uid, packageName, instanceId)
+ interactor.removeMediaRecommendations(key, dismissIntent, GUTS_DISMISS_DELAY_MS_DURATION)
+ }
+
+ private fun onClicked(
+ expandable: Expandable,
+ intent: Intent?,
+ packageName: String,
+ instanceId: InstanceId?,
+ index: Int
+ ) {
+ if (intent == null || intent.extras == null) {
+ Log.e(TAG, "No tap action can be set up")
+ return
+ }
+
+ if (index == -1) {
+ logger.logRecommendationCardTap(packageName, instanceId)
+ } else {
+ logger.logRecommendationItemTap(packageName, instanceId, index)
+ }
+ // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_CLICK_EVENT).
+ interactor.startClickIntent(expandable, intent)
+ }
+
+ private suspend fun toRecsViewModel(model: MediaRecommendationsModel): MediaRecsCardViewModel? {
+ if (!model.areRecommendationsValid) {
+ Log.e(TAG, "Received an invalid recommendation list")
+ return null
+ }
+ if (model.appName == null || model.uid == Process.INVALID_UID) {
+ Log.w(TAG, "Fail to get media recommendation's app info")
+ return null
+ }
+
+ val scheme = getColorScheme(model.packageName) ?: return null
+
+ // Capture width & height from views in foreground for artwork scaling in background
+ val width =
+ applicationContext.resources.getDimensionPixelSize(R.dimen.qs_media_rec_album_width)
+ val height =
+ applicationContext.resources.getDimensionPixelSize(
+ R.dimen.qs_media_rec_album_height_expanded
+ )
+
+ val appIcon = applicationContext.packageManager.getApplicationIcon(model.packageName)
+ val textPrimaryColor = textPrimaryFromScheme(scheme)
+ val textSecondaryColor = textSecondaryFromScheme(scheme)
+ val backgroundColor = surfaceFromScheme(scheme)
+
+ var areTitlesVisible = false
+ var areSubtitlesVisible = false
+ val mediaRecs =
+ model.mediaRecs.map { mediaRecModel ->
+ areTitlesVisible = areTitlesVisible || !mediaRecModel.title.isNullOrEmpty()
+ areSubtitlesVisible = areSubtitlesVisible || !mediaRecModel.subtitle.isNullOrEmpty()
+ val progress = MediaDataUtils.getDescriptionProgress(mediaRecModel.extras) ?: 0.0
+ MediaRecViewModel(
+ contentDescription =
+ setUpMediaRecContentDescription(mediaRecModel, model.appName),
+ title = mediaRecModel.title ?: "",
+ titleColor = textPrimaryColor,
+ subtitle = mediaRecModel.subtitle ?: "",
+ subtitleColor = textSecondaryColor,
+ progress = (progress * 100).toInt(),
+ progressColor = textPrimaryColor,
+ albumIcon =
+ getRecCoverBackground(
+ mediaRecModel.icon,
+ width,
+ height,
+ ),
+ appIcon = appIcon,
+ onClicked = { expandable, index ->
+ onClicked(
+ expandable,
+ mediaRecModel.intent,
+ model.packageName,
+ model.instanceId,
+ index,
+ )
+ }
+ )
+ }
+ // Subtitles should only be visible if titles are visible.
+ areSubtitlesVisible = areTitlesVisible && areSubtitlesVisible
+
+ return MediaRecsCardViewModel(
+ contentDescription = { gutsVisible ->
+ if (gutsVisible) {
+ applicationContext.getString(
+ R.string.controls_media_close_session,
+ model.appName
+ )
+ } else {
+ applicationContext.getString(R.string.controls_media_smartspace_rec_header)
+ }
+ },
+ cardColor = backgroundColor,
+ cardTitleColor = textPrimaryColor,
+ onClicked = { expandable ->
+ onClicked(
+ expandable,
+ model.dismissIntent,
+ model.packageName,
+ model.instanceId,
+ index = -1
+ )
+ },
+ onLongClicked = {
+ logger.logLongPressOpen(model.uid, model.packageName, model.instanceId)
+ },
+ mediaRecs = mediaRecs,
+ areTitlesVisible = areTitlesVisible,
+ areSubtitlesVisible = areSubtitlesVisible,
+ gutsMenu = toGutsViewModel(model, scheme),
+ )
+ }
+
+ private fun toGutsViewModel(
+ model: MediaRecommendationsModel,
+ scheme: ColorScheme
+ ): GutsViewModel {
+ return GutsViewModel(
+ gutsText =
+ applicationContext.getString(R.string.controls_media_close_session, model.appName),
+ textColor = textPrimaryFromScheme(scheme),
+ buttonBackgroundColor = accentPrimaryFromScheme(scheme),
+ buttonTextColor = surfaceFromScheme(scheme),
+ onDismissClicked = {
+ onMediaRecommendationsDismissed(
+ model.key,
+ model.uid,
+ model.packageName,
+ model.dismissIntent,
+ model.instanceId
+ )
+ },
+ cancelTextBackground =
+ applicationContext.getDrawable(R.drawable.qs_media_outline_button),
+ onSettingsClicked = {
+ logger.logLongPressSettings(model.uid, model.packageName, model.instanceId)
+ interactor.startSettings()
+ },
+ )
+ }
+
+ /** Returns the recommendation album cover of [width]x[height] size. */
+ private suspend fun getRecCoverBackground(icon: Icon?, width: Int, height: Int): Drawable =
+ withContext(backgroundDispatcher) {
+ return@withContext MediaArtworkHelper.getWallpaperColor(
+ applicationContext,
+ backgroundDispatcher,
+ icon,
+ TAG,
+ )
+ ?.let { wallpaperColors ->
+ addGradientToRecommendationAlbum(
+ icon!!,
+ ColorScheme(wallpaperColors, true, Style.CONTENT),
+ width,
+ height
+ )
+ }
+ ?: ColorDrawable(Color.TRANSPARENT)
+ }
+
+ private fun addGradientToRecommendationAlbum(
+ artworkIcon: Icon,
+ mutableColorScheme: ColorScheme,
+ width: Int,
+ height: Int
+ ): LayerDrawable {
+ // First try scaling rec card using bitmap drawable.
+ // If returns null, set drawable bounds.
+ val albumArt =
+ getScaledRecommendationCover(artworkIcon, width, height)
+ ?: MediaArtworkHelper.getScaledBackground(
+ applicationContext,
+ artworkIcon,
+ width,
+ height
+ )
+ val gradient =
+ AppCompatResources.getDrawable(applicationContext, R.drawable.qs_media_rec_scrim)
+ ?.mutate() as GradientDrawable
+ return MediaArtworkHelper.setUpGradientColorOnDrawable(
+ albumArt,
+ gradient,
+ mutableColorScheme,
+ MEDIA_REC_SCRIM_START_ALPHA,
+ MEDIA_REC_SCRIM_END_ALPHA
+ )
+ }
+
+ private fun setUpMediaRecContentDescription(
+ mediaRec: MediaRecModel,
+ appName: CharSequence?
+ ): CharSequence {
+ // Set up the accessibility label for the media item.
+ val artistName = mediaRec.extras?.getString(KEY_SMARTSPACE_ARTIST_NAME, "")
+ return if (artistName.isNullOrEmpty()) {
+ applicationContext.getString(
+ R.string.controls_media_smartspace_rec_item_no_artist_description,
+ mediaRec.title,
+ appName
+ )
+ } else {
+ applicationContext.getString(
+ R.string.controls_media_smartspace_rec_item_description,
+ mediaRec.title,
+ artistName,
+ appName
+ )
+ }
+ }
+
+ private fun getColorScheme(packageName: String): ColorScheme? {
+ // Set up recommendation card's header.
+ return try {
+ val packageManager = applicationContext.packageManager
+ val applicationInfo = packageManager.getApplicationInfo(packageName, 0 /* flags */)
+ // Set up media source app's logo.
+ val icon = packageManager.getApplicationIcon(applicationInfo)
+ ColorScheme(WallpaperColors.fromDrawable(icon), darkTheme = true)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Fail to get media recommendation's app info", e)
+ null
+ }
+ }
+
+ /** Returns a [Drawable] of a given [artworkIcon] scaled to [width]x[height] size, . */
+ private fun getScaledRecommendationCover(
+ artworkIcon: Icon,
+ width: Int,
+ height: Int
+ ): Drawable? {
+ check(width > 0) { "Width must be a positive number but was $width" }
+ check(height > 0) { "Height must be a positive number but was $height" }
+
+ return if (
+ artworkIcon.type == Icon.TYPE_BITMAP || artworkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP
+ ) {
+ artworkIcon.bitmap?.let {
+ val bitmap = Bitmap.createScaledBitmap(it, width, height, false)
+ BitmapDrawable(applicationContext.resources, bitmap)
+ }
+ } else {
+ null
+ }
+ }
+
+ companion object {
+ private const val TAG = "MediaRecommendationsViewModel"
+ private const val KEY_SMARTSPACE_ARTIST_NAME = "artist_name"
+ private const val MEDIA_REC_SCRIM_START_ALPHA = 0.15f
+ private const val MEDIA_REC_SCRIM_END_ALPHA = 1.0f
+ /**
+ * Delay duration is based on [GUTS_ANIMATION_DURATION], it should have 100 ms increase in
+ * order to let the animation end.
+ */
+ private const val GUTS_DISMISS_DELAY_MS_DURATION = 334L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecsCardViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecsCardViewModel.kt
new file mode 100644
index 0000000..d1713b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecsCardViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.annotation.ColorInt
+import com.android.systemui.animation.Expandable
+
+/** Models UI state for media recommendations card. */
+data class MediaRecsCardViewModel(
+ val contentDescription: (Boolean) -> CharSequence,
+ @ColorInt val cardColor: Int,
+ @ColorInt val cardTitleColor: Int,
+ val onClicked: (Expandable) -> Unit,
+ val onLongClicked: () -> Unit,
+ val mediaRecs: List<MediaRecViewModel>,
+ val areTitlesVisible: Boolean,
+ val areSubtitlesVisible: Boolean,
+ val gutsMenu: GutsViewModel,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
index 2c25fe2..09f973c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
@@ -99,15 +99,15 @@
logger.log(MediaUiEvent.DISMISS_SWIPE)
}
- fun logLongPressOpen(uid: Int, packageName: String, instanceId: InstanceId) {
+ fun logLongPressOpen(uid: Int, packageName: String, instanceId: InstanceId?) {
logger.logWithInstanceId(MediaUiEvent.OPEN_LONG_PRESS, uid, packageName, instanceId)
}
- fun logLongPressDismiss(uid: Int, packageName: String, instanceId: InstanceId) {
+ fun logLongPressDismiss(uid: Int, packageName: String, instanceId: InstanceId?) {
logger.logWithInstanceId(MediaUiEvent.DISMISS_LONG_PRESS, uid, packageName, instanceId)
}
- fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) {
+ fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId?) {
logger.logWithInstanceId(
MediaUiEvent.OPEN_SETTINGS_LONG_PRESS,
uid,
@@ -188,7 +188,7 @@
)
}
- fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) {
+ fun logRecommendationItemTap(packageName: String, instanceId: InstanceId?, position: Int) {
logger.logWithInstanceIdAndPosition(
MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP,
0,
@@ -198,7 +198,7 @@
)
}
- fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) {
+ fun logRecommendationCardTap(packageName: String, instanceId: InstanceId?) {
logger.logWithInstanceId(
MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP,
0,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
index eb0870a..2b7df7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
@@ -75,6 +75,8 @@
private val notificationEntry = notification.entry
private val notificationKey = notificationEntry.sbn.key
+ override val isLaunching: Boolean = true
+
override var transitionContainer: ViewGroup
get() = notification.rootView as ViewGroup
set(ignored) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 317c063..8f00b43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -44,9 +44,6 @@
/** The carrierId for this connection. See [TelephonyManager.getSimCarrierId] */
val carrierId: StateFlow<Int>
- /** Reflects the value from the carrier config INFLATE_SIGNAL_STRENGTH for this connection */
- val inflateSignalStrength: StateFlow<Boolean>
-
/**
* The table log buffer created for this connection. Will have the name "MobileConnectionLog
* [subId]"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
index 90cdfeb..af34a57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
@@ -43,7 +43,6 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/**
@@ -68,17 +67,6 @@
)
.stateIn(scope, SharingStarted.WhileSubscribed(), _carrierId.value)
- private val _inflateSignalStrength: MutableStateFlow<Boolean> = MutableStateFlow(false)
- override val inflateSignalStrength =
- _inflateSignalStrength
- .logDiffsForTable(
- tableLogBuffer,
- columnPrefix = "",
- columnName = "inflate",
- _inflateSignalStrength.value
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value)
-
private val _isEmergencyOnly = MutableStateFlow(false)
override val isEmergencyOnly =
_isEmergencyOnly
@@ -203,16 +191,7 @@
.logDiffsForTable(tableLogBuffer, columnPrefix = "", _resolvedNetworkType.value)
.stateIn(scope, SharingStarted.WhileSubscribed(), _resolvedNetworkType.value)
- override val numberOfLevels =
- _inflateSignalStrength
- .map { shouldInflate ->
- if (shouldInflate) {
- DEFAULT_NUM_LEVELS + 1
- } else {
- DEFAULT_NUM_LEVELS
- }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
+ override val numberOfLevels = MutableStateFlow(MobileConnectionRepository.DEFAULT_NUM_LEVELS)
override val dataEnabled = MutableStateFlow(true)
@@ -247,7 +226,8 @@
_carrierId.value = event.carrierId ?: INVALID_SUBSCRIPTION_ID
- _inflateSignalStrength.value = event.inflateStrength
+ numberOfLevels.value =
+ if (event.inflateStrength) DEFAULT_NUM_LEVELS + 1 else DEFAULT_NUM_LEVELS
cdmaRoaming.value = event.roaming
_isRoaming.value = event.roaming
@@ -278,6 +258,7 @@
carrierName.value = NetworkNameModel.SubscriptionDerived(CARRIER_MERGED_NAME)
// TODO(b/276943904): is carrierId a thing with carrier merged networks?
_carrierId.value = INVALID_SUBSCRIPTION_ID
+ numberOfLevels.value = event.numberOfLevels
cdmaRoaming.value = false
_primaryLevel.value = event.level
_cdmaLevel.value = event.level
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
index cb99d11..2bc3bcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -165,7 +165,6 @@
override val isRoaming = MutableStateFlow(false).asStateFlow()
override val carrierId = MutableStateFlow(INVALID_SUBSCRIPTION_ID).asStateFlow()
- override val inflateSignalStrength = MutableStateFlow(false).asStateFlow()
override val isEmergencyOnly = MutableStateFlow(false).asStateFlow()
override val operatorAlphaShort = MutableStateFlow(null).asStateFlow()
override val isInService = MutableStateFlow(true).asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index bb0af77..b085d80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -291,21 +291,6 @@
)
.stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value)
- override val inflateSignalStrength =
- activeRepo
- .flatMapLatest { it.inflateSignalStrength }
- .logDiffsForTable(
- tableLogBuffer,
- columnPrefix = "",
- columnName = "inflate",
- initialValue = activeRepo.value.inflateSignalStrength.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.inflateSignalStrength.value
- )
-
override val numberOfLevels =
activeRepo
.flatMapLatest { it.numberOfLevels }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 2cbe965..5ab2ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -302,10 +302,8 @@
}
.stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType)
- override val inflateSignalStrength = systemUiCarrierConfig.shouldInflateSignalStrength
-
override val numberOfLevels =
- inflateSignalStrength
+ systemUiCarrierConfig.shouldInflateSignalStrength
.map { shouldInflate ->
if (shouldInflate) {
DEFAULT_NUM_LEVELS + 1
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index cbebfd0..9d194cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -367,11 +367,8 @@
combine(
level,
isInService,
- connectionRepository.inflateSignalStrength,
- ) { level, isInService, inflate ->
- if (isInService) {
- if (inflate) level + 1 else level
- } else 0
+ ) { level, isInService ->
+ if (isInService) level else 0
}
.stateIn(scope, SharingStarted.WhileSubscribed(), 0)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt
index d6b8fd4..5d3b9ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt
@@ -94,7 +94,8 @@
}
override fun logFully(row: TableRowLogger) {
- row.logChange("numLevels", "HELLO")
+ // Satellite icon has only 3 levels, unchanging
+ row.logChange(COL_NUM_LEVELS, "3")
row.logChange(COL_TYPE, "s")
row.logChange(COL_LEVEL, level)
}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 2f2c4b0..b6f5433 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -31,6 +31,7 @@
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
+import android.view.accessibility.AccessibilityNodeInfo
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DimenRes
@@ -57,6 +58,7 @@
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLock
+import java.time.Duration
import javax.inject.Inject
/**
@@ -228,6 +230,18 @@
chipInnerView.contentDescription =
"$loadedIconDesc${newInfo.text.loadText(context)}$endItemDesc"
chipInnerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE
+ // Set minimum duration between content changes to 1 second in order to announce quick
+ // state changes.
+ chipInnerView.accessibilityDelegate =
+ object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.minDurationBetweenContentChanges = Duration.ofMillis(1000)
+ }
+ }
maybeGetAccessibilityFocus(newInfo, currentView)
// ---- Haptics ----
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 572a6c1..0dbbe63 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -224,6 +224,5 @@
<instrumentation android:name="android.testing.TestableInstrumentation"
android:targetPackage="com.android.systemui.tests"
- android:label="Tests for SystemUI">
- </instrumentation>
+ android:label="Tests for SystemUI" />
</manifest>
diff --git a/packages/SystemUI/tests/AndroidTest.xml b/packages/SystemUI/tests/AndroidTest.xml
index cd2a62d..2de5faf 100644
--- a/packages/SystemUI/tests/AndroidTest.xml
+++ b/packages/SystemUI/tests/AndroidTest.xml
@@ -23,6 +23,15 @@
<option name="force-root" value="true" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="screen-always-on" value="on" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
+
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="framework-base-presubmit" />
<option name="test-tag" value="SystemUITests" />
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching.json b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching.json
new file mode 100644
index 0000000..60bff17
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenLaunching.json
@@ -0,0 +1,393 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 26,
+ 52,
+ 78,
+ 105,
+ 131,
+ 157,
+ 184,
+ 210,
+ 236,
+ 263,
+ 289,
+ 315,
+ 342,
+ 368,
+ 394,
+ 421,
+ 447,
+ 473,
+ 500
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "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": 91,
+ "top": 269,
+ "right": 213,
+ "bottom": 430
+ },
+ {
+ "left": 71,
+ "top": 206,
+ "right": 240,
+ "bottom": 491
+ },
+ {
+ "left": 34,
+ "top": 98,
+ "right": 283,
+ "bottom": 595
+ },
+ {
+ "left": 22,
+ "top": 63,
+ "right": 296,
+ "bottom": 629
+ },
+ {
+ "left": 15,
+ "top": 44,
+ "right": 303,
+ "bottom": 648
+ },
+ {
+ "left": 11,
+ "top": 32,
+ "right": 308,
+ "bottom": 659
+ },
+ {
+ "left": 8,
+ "top": 23,
+ "right": 311,
+ "bottom": 667
+ },
+ {
+ "left": 6,
+ "top": 18,
+ "right": 313,
+ "bottom": 673
+ },
+ {
+ "left": 5,
+ "top": 13,
+ "right": 315,
+ "bottom": 677
+ },
+ {
+ "left": 3,
+ "top": 9,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 7,
+ "right": 317,
+ "bottom": 683
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 319,
+ "bottom": 687
+ },
+ {
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 10,
+ "top_left_y": 10,
+ "top_right_x": 10,
+ "top_right_y": 10,
+ "bottom_right_x": 20,
+ "bottom_right_y": 20,
+ "bottom_left_x": 20,
+ "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": 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": 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": 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": 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": 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": 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": 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": 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": 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.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.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.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.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.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.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.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.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,
+ "top_left_y": 0,
+ "top_right_x": 0,
+ "top_right_y": 0,
+ "bottom_right_x": 0,
+ "bottom_right_y": 0,
+ "bottom_left_x": 0,
+ "bottom_left_y": 0
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 0,
+ 115,
+ 178,
+ 217,
+ 241,
+ 253,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning.json b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning.json
new file mode 100644
index 0000000..ea768c0
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimationWithoutFade_whenReturning.json
@@ -0,0 +1,393 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 26,
+ 52,
+ 78,
+ 105,
+ 131,
+ 157,
+ 184,
+ 210,
+ 236,
+ 263,
+ 289,
+ 315,
+ 342,
+ 368,
+ 394,
+ 421,
+ 447,
+ 473,
+ 500
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "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": 91,
+ "top": 269,
+ "right": 213,
+ "bottom": 430
+ },
+ {
+ "left": 71,
+ "top": 206,
+ "right": 240,
+ "bottom": 491
+ },
+ {
+ "left": 34,
+ "top": 98,
+ "right": 283,
+ "bottom": 595
+ },
+ {
+ "left": 22,
+ "top": 63,
+ "right": 296,
+ "bottom": 629
+ },
+ {
+ "left": 15,
+ "top": 44,
+ "right": 303,
+ "bottom": 648
+ },
+ {
+ "left": 11,
+ "top": 32,
+ "right": 308,
+ "bottom": 659
+ },
+ {
+ "left": 8,
+ "top": 23,
+ "right": 311,
+ "bottom": 667
+ },
+ {
+ "left": 6,
+ "top": 18,
+ "right": 313,
+ "bottom": 673
+ },
+ {
+ "left": 5,
+ "top": 13,
+ "right": 315,
+ "bottom": 677
+ },
+ {
+ "left": 3,
+ "top": 9,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 7,
+ "right": 317,
+ "bottom": 683
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 319,
+ "bottom": 687
+ },
+ {
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 10,
+ "top_left_y": 10,
+ "top_right_x": 10,
+ "top_right_y": 10,
+ "bottom_right_x": 20,
+ "bottom_right_y": 20,
+ "bottom_left_x": 20,
+ "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": 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": 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": 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": 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": 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": 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": 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": 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": 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.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.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.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.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.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.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.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.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,
+ "top_left_y": 0,
+ "top_right_x": 0,
+ "top_right_y": 0,
+ "bottom_right_x": 0,
+ "bottom_right_y": 0,
+ "bottom_left_x": 0,
+ "bottom_left_y": 0
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 255,
+ 239,
+ 183,
+ 135,
+ 91,
+ 53,
+ 23,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0,
+ 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
new file mode 100644
index 0000000..608e633
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimation_whenLaunching.json
@@ -0,0 +1,393 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 26,
+ 52,
+ 78,
+ 105,
+ 131,
+ 157,
+ 184,
+ 210,
+ 236,
+ 263,
+ 289,
+ 315,
+ 342,
+ 368,
+ 394,
+ 421,
+ 447,
+ 473,
+ 500
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "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": 91,
+ "top": 269,
+ "right": 213,
+ "bottom": 430
+ },
+ {
+ "left": 71,
+ "top": 206,
+ "right": 240,
+ "bottom": 491
+ },
+ {
+ "left": 34,
+ "top": 98,
+ "right": 283,
+ "bottom": 595
+ },
+ {
+ "left": 22,
+ "top": 63,
+ "right": 296,
+ "bottom": 629
+ },
+ {
+ "left": 15,
+ "top": 44,
+ "right": 303,
+ "bottom": 648
+ },
+ {
+ "left": 11,
+ "top": 32,
+ "right": 308,
+ "bottom": 659
+ },
+ {
+ "left": 8,
+ "top": 23,
+ "right": 311,
+ "bottom": 667
+ },
+ {
+ "left": 6,
+ "top": 18,
+ "right": 313,
+ "bottom": 673
+ },
+ {
+ "left": 5,
+ "top": 13,
+ "right": 315,
+ "bottom": 677
+ },
+ {
+ "left": 3,
+ "top": 9,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 7,
+ "right": 317,
+ "bottom": 683
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 319,
+ "bottom": 687
+ },
+ {
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 10,
+ "top_left_y": 10,
+ "top_right_x": 10,
+ "top_right_y": 10,
+ "bottom_right_x": 20,
+ "bottom_right_y": 20,
+ "bottom_left_x": 20,
+ "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": 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": 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": 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": 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": 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": 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": 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": 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": 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.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.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.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.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.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.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.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.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,
+ "top_left_y": 0,
+ "top_right_x": 0,
+ "top_right_y": 0,
+ "bottom_right_x": 0,
+ "bottom_right_y": 0,
+ "bottom_left_x": 0,
+ "bottom_left_y": 0
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 0,
+ 115,
+ 178,
+ 217,
+ 241,
+ 253,
+ 239,
+ 183,
+ 135,
+ 91,
+ 53,
+ 23,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0,
+ 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
new file mode 100644
index 0000000..608e633
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/backgroundAnimation_whenReturning.json
@@ -0,0 +1,393 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 26,
+ 52,
+ 78,
+ 105,
+ 131,
+ 157,
+ 184,
+ 210,
+ 236,
+ 263,
+ 289,
+ 315,
+ 342,
+ 368,
+ 394,
+ 421,
+ 447,
+ 473,
+ 500
+ ],
+ "features": [
+ {
+ "name": "bounds",
+ "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": 91,
+ "top": 269,
+ "right": 213,
+ "bottom": 430
+ },
+ {
+ "left": 71,
+ "top": 206,
+ "right": 240,
+ "bottom": 491
+ },
+ {
+ "left": 34,
+ "top": 98,
+ "right": 283,
+ "bottom": 595
+ },
+ {
+ "left": 22,
+ "top": 63,
+ "right": 296,
+ "bottom": 629
+ },
+ {
+ "left": 15,
+ "top": 44,
+ "right": 303,
+ "bottom": 648
+ },
+ {
+ "left": 11,
+ "top": 32,
+ "right": 308,
+ "bottom": 659
+ },
+ {
+ "left": 8,
+ "top": 23,
+ "right": 311,
+ "bottom": 667
+ },
+ {
+ "left": 6,
+ "top": 18,
+ "right": 313,
+ "bottom": 673
+ },
+ {
+ "left": 5,
+ "top": 13,
+ "right": 315,
+ "bottom": 677
+ },
+ {
+ "left": 3,
+ "top": 9,
+ "right": 316,
+ "bottom": 681
+ },
+ {
+ "left": 2,
+ "top": 7,
+ "right": 317,
+ "bottom": 683
+ },
+ {
+ "left": 2,
+ "top": 5,
+ "right": 318,
+ "bottom": 685
+ },
+ {
+ "left": 1,
+ "top": 3,
+ "right": 319,
+ "bottom": 687
+ },
+ {
+ "left": 1,
+ "top": 2,
+ "right": 319,
+ "bottom": 688
+ },
+ {
+ "left": 0,
+ "top": 1,
+ "right": 320,
+ "bottom": 689
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ },
+ {
+ "left": 0,
+ "top": 0,
+ "right": 320,
+ "bottom": 690
+ }
+ ]
+ },
+ {
+ "name": "corner_radii",
+ "type": "cornerRadii",
+ "data_points": [
+ null,
+ {
+ "top_left_x": 10,
+ "top_left_y": 10,
+ "top_right_x": 10,
+ "top_right_y": 10,
+ "bottom_right_x": 20,
+ "bottom_right_y": 20,
+ "bottom_left_x": 20,
+ "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": 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": 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": 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": 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": 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": 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": 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": 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": 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.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.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.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.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.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.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.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.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,
+ "top_left_y": 0,
+ "top_right_x": 0,
+ "top_right_y": 0,
+ "bottom_right_x": 0,
+ "bottom_right_y": 0,
+ "bottom_left_x": 0,
+ "bottom_left_y": 0
+ }
+ ]
+ },
+ {
+ "name": "alpha",
+ "type": "int",
+ "data_points": [
+ 0,
+ 0,
+ 115,
+ 178,
+ 217,
+ 241,
+ 253,
+ 239,
+ 183,
+ 135,
+ 91,
+ 53,
+ 23,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 75a49d7..41974f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -246,6 +246,8 @@
*/
private class TestTransitionAnimatorController(override var transitionContainer: ViewGroup) :
ActivityTransitionAnimator.Controller {
+ override val isLaunching: Boolean = true
+
override fun createAnimatorState() =
TransitionAnimator.State(
top = 100,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
new file mode 100644
index 0000000..c380a51
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.animation
+
+import android.animation.AnimatorSet
+import android.graphics.drawable.GradientDrawable
+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 com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.EmptyTestActivity
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.motion.RecordedMotion
+import platform.test.motion.Sampling.Companion.evenlySampled
+import platform.test.motion.view.DrawableFeatureCaptures
+import platform.test.motion.view.ViewMotionTestRule
+import platform.test.screenshot.DeviceEmulationRule
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.DisplaySpec
+import platform.test.screenshot.GoldenPathManager
+import platform.test.screenshot.PathConfig
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TransitionAnimatorTest : 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,
+ )
+ )
+ }
+
+ private val pathManager = GoldenPathManager(context, GOLDENS_PATH, pathConfig = PathConfig())
+ private val transitionAnimator =
+ TransitionAnimator(
+ ActivityTransitionAnimator.TIMINGS,
+ ActivityTransitionAnimator.INTERPOLATORS
+ )
+
+ @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
+ @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
+ @get:Rule(order = 2)
+ val motionRule =
+ ViewMotionTestRule<EmptyTestActivity>(
+ pathManager,
+ { activityRule.scenario },
+ context = context,
+ bitmapDiffer = null,
+ )
+
+ @Test
+ fun backgroundAnimation_whenLaunching() {
+ val backgroundLayer = GradientDrawable().apply { alpha = 0 }
+ val animator = setUpTest(backgroundLayer, isLaunching = true)
+
+ val recordedMotion = recordMotion(backgroundLayer, animator)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ }
+
+ @Test
+ fun backgroundAnimation_whenReturning() {
+ val backgroundLayer = GradientDrawable().apply { alpha = 0 }
+ val animator = setUpTest(backgroundLayer, isLaunching = false)
+
+ val recordedMotion = recordMotion(backgroundLayer, animator)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ }
+
+ @Test
+ fun backgroundAnimationWithoutFade_whenLaunching() {
+ val backgroundLayer = GradientDrawable().apply { alpha = 0 }
+ val animator =
+ setUpTest(backgroundLayer, isLaunching = true, fadeWindowBackgroundLayer = false)
+
+ val recordedMotion = recordMotion(backgroundLayer, animator)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ }
+
+ @Test
+ fun backgroundAnimationWithoutFade_whenReturning() {
+ val backgroundLayer = GradientDrawable().apply { alpha = 0 }
+ val animator =
+ setUpTest(backgroundLayer, isLaunching = false, fadeWindowBackgroundLayer = false)
+
+ val recordedMotion = recordMotion(backgroundLayer, animator)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden()
+ }
+
+ private fun setUpTest(
+ backgroundLayer: GradientDrawable,
+ isLaunching: Boolean,
+ fadeWindowBackgroundLayer: Boolean = true,
+ ): AnimatorSet {
+ lateinit var transitionContainer: ViewGroup
+ activityRule.scenario.onActivity { activity ->
+ transitionContainer = FrameLayout(activity).apply { setBackgroundColor(0x00FF00) }
+ activity.setContentView(transitionContainer)
+ }
+ waitForIdleSync()
+
+ val controller = TestController(transitionContainer, isLaunching)
+ val animator =
+ transitionAnimator.createAnimator(
+ controller,
+ createEndState(transitionContainer),
+ backgroundLayer,
+ fadeWindowBackgroundLayer
+ )
+ return AnimatorSet().apply {
+ duration = animator.duration
+ play(animator)
+ }
+ }
+
+ private fun createEndState(container: ViewGroup): TransitionAnimator.State {
+ val containerLocation = IntArray(2)
+ container.getLocationOnScreen(containerLocation)
+ return TransitionAnimator.State(
+ left = containerLocation[0],
+ top = containerLocation[1],
+ right = containerLocation[0] + emulationSpec.display.width,
+ bottom = containerLocation[1] + emulationSpec.display.height,
+ topCornerRadius = 0f,
+ bottomCornerRadius = 0f
+ )
+ }
+
+ private fun recordMotion(
+ backgroundLayer: GradientDrawable,
+ animator: AnimatorSet
+ ): RecordedMotion {
+ return motionRule.checkThat(animator).record(
+ backgroundLayer,
+ evenlySampled(20),
+ visualCapture = null
+ ) {
+ capture(DrawableFeatureCaptures.bounds, "bounds")
+ capture(DrawableFeatureCaptures.cornerRadii, "corner_radii")
+ capture(DrawableFeatureCaptures.alpha, "alpha")
+ }
+ }
+}
+
+/**
+ * A simple implementation of [TransitionAnimator.Controller] which throws if it is called outside
+ * of the main thread.
+ */
+private class TestController(
+ override var transitionContainer: ViewGroup,
+ override val isLaunching: Boolean
+) : TransitionAnimator.Controller {
+ override fun createAnimatorState(): TransitionAnimator.State {
+ val containerLocation = IntArray(2)
+ transitionContainer.getLocationOnScreen(containerLocation)
+ return TransitionAnimator.State(
+ left = containerLocation[0] + 100,
+ top = containerLocation[1] + 300,
+ right = containerLocation[0] + 200,
+ bottom = containerLocation[1] + 400,
+ topCornerRadius = 10f,
+ bottomCornerRadius = 20f
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index 3926f92..0a7e72c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -288,4 +288,16 @@
assertThat(transitionRepository)
.startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE)
}
+
+ @Test
+ fun testTransitionToOccluded_onWake() =
+ testScope.runTest {
+ kosmos.fakeKeyguardRepository.setKeyguardOccluded(true)
+ powerInteractor.setAwakeForTest()
+ runCurrent()
+
+ // Waking up from AOD while occluded should transition to OCCLUDED.
+ assertThat(transitionRepository)
+ .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 722387c..02954b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -28,7 +28,7 @@
import android.content.pm.ApplicationInfo;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
-import androidx.test.filters.FlakyTest;
+
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase;
@@ -46,11 +46,10 @@
import java.util.Collections;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
-@FlakyTest(bugId = 327655994) // Also b/324682425
@SmallTest
@RunWith(AndroidJUnit4.class)
public class PluginInstanceTest extends SysuiTestCase {
@@ -177,7 +176,7 @@
}
@Test
- public void testLoadUnloadSimultaneous_HoldsUnload() throws Exception {
+ public void testLoadUnloadSimultaneous_HoldsUnload() throws Throwable {
final Semaphore loadLock = new Semaphore(1);
final Semaphore unloadLock = new Semaphore(1);
@@ -190,16 +189,16 @@
Thread.yield();
boolean isLocked = getLock(unloadLock, 1000);
- // Ensure the bg thread failed to do delete the plugin
+ // Ensure the bg thread failed to delete the plugin
assertNotNull(mPluginInstance.getPlugin());
// We expect that bgThread deadlocked holding the semaphore
assertFalse(isLocked);
};
- AtomicBoolean isBgThreadFailed = new AtomicBoolean(false);
+ AtomicReference<Throwable> bgFailure = new AtomicReference<Throwable>(null);
Thread bgThread = new Thread(() -> {
assertTrue(getLock(unloadLock, 10));
- assertTrue(getLock(loadLock, 4000)); // Wait for the foreground thread
+ assertTrue(getLock(loadLock, 10000)); // Wait for the foreground thread
assertNotNull(mPluginInstance.getPlugin());
// Attempt to delete the plugin, this should block until the load completes
mPluginInstance.unloadPlugin();
@@ -210,8 +209,9 @@
// This protects the test suite from crashing due to the uncaught exception.
bgThread.setUncaughtExceptionHandler((Thread t, Throwable ex) -> {
- Log.e("testLoadUnloadSimultaneous_HoldsUnload", "Exception from BG Thread", ex);
- isBgThreadFailed.set(true);
+ Log.e("PluginInstanceTest#testLoadUnloadSimultaneous_HoldsUnload",
+ "Exception from BG Thread", ex);
+ bgFailure.set(ex);
});
loadLock.acquire();
@@ -222,7 +222,13 @@
mPluginInstance.loadPlugin();
bgThread.join(5000);
- assertFalse(isBgThreadFailed.get());
+
+ // Rethrow final background exception on test thread
+ Throwable bgEx = bgFailure.get();
+ if (bgEx != null) {
+ throw bgEx;
+ }
+
assertNull(mPluginInstance.getPlugin());
}
@@ -230,6 +236,8 @@
try {
return lock.tryAcquire(millis, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
+ Log.e("PluginInstanceTest#getLock",
+ "Interrupted Exception getting lock", ex);
fail();
return false;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
index 3c13906..c13e830 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.net.ConnectivityManager
-import android.os.PersistableBundle
import android.telephony.ServiceState
import android.telephony.SignalStrength
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
@@ -100,9 +99,6 @@
)
)
- // Use a real config, with no overrides
- private val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_ID, PersistableBundle())
-
private lateinit var mobileRepo: FakeMobileConnectionRepository
private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
@@ -684,6 +680,10 @@
telephonyManager: TelephonyManager,
): MobileConnectionRepositoryImpl {
whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID)
+ val systemUiCarrierConfigMock: SystemUiCarrierConfig = mock()
+ whenever(systemUiCarrierConfigMock.satelliteConnectionHysteresisSeconds)
+ .thenReturn(MutableStateFlow(0))
+
val realRepo =
MobileConnectionRepositoryImpl(
SUB_ID,
@@ -693,7 +693,7 @@
SEP,
connectivityManager,
telephonyManager,
- systemUiCarrierConfig = systemUiCarrierConfig,
+ systemUiCarrierConfig = systemUiCarrierConfigMock,
fakeBroadcastDispatcher,
mobileMappingsProxy = mock(),
testDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 9d14116..f761bcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -1030,26 +1030,6 @@
}
@Test
- fun inflateSignalStrength_usesCarrierConfig() =
- testScope.runTest {
- val latest by collectLastValue(underTest.inflateSignalStrength)
-
- assertThat(latest).isEqualTo(false)
-
- systemUiCarrierConfig.processNewCarrierConfig(
- configWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true)
- )
-
- assertThat(latest).isEqualTo(true)
-
- systemUiCarrierConfig.processNewCarrierConfig(
- configWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
- )
-
- assertThat(latest).isEqualTo(false)
- }
-
- @Test
fun isAllowedDuringAirplaneMode_alwaysFalse() =
testScope.runTest {
val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index f9ab25e..c49fcf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -181,22 +181,6 @@
}
@Test
- fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() =
- testScope.runTest {
- connectionRepository.inflateSignalStrength.value = false
- val latest by collectLastValue(underTest.signalLevelIcon)
-
- connectionRepository.primaryLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
-
- connectionRepository.inflateSignalStrength.value = true
- connectionRepository.primaryLevel.value = 4
-
- // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level
- assertThat(latest!!.level).isEqualTo(5)
- }
-
- @Test
fun iconGroup_three_g() =
testScope.runTest {
connectionRepository.resolvedNetworkType.value =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/BroadcastSenderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/BroadcastSenderKosmos.kt
new file mode 100644
index 0000000..42ad679
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/BroadcastSenderKosmos.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.systemui.broadcast
+
+import android.content.applicationContext
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.wakelock.WakeLockFake
+
+val Kosmos.mockBroadcastSender by Kosmos.Fixture { mock<BroadcastSender>() }
+var Kosmos.broadcastSender by
+ Kosmos.Fixture {
+ BroadcastSender(applicationContext, WakeLockFake.Builder(applicationContext), fakeExecutor)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractorKosmos.kt
index 372a1961..1edd405 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractorKosmos.kt
@@ -17,10 +17,12 @@
package com.android.systemui.media.controls.domain.pipeline.interactor
import android.content.applicationContext
+import com.android.systemui.broadcast.broadcastSender
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.plugins.activityStarter
val Kosmos.mediaRecommendationsInteractor by
Kosmos.Fixture {
@@ -29,5 +31,7 @@
applicationContext = applicationContext,
repository = mediaFilterRepository,
mediaDataProcessor = mediaDataProcessor,
+ broadcastSender = broadcastSender,
+ activityStarter = activityStarter,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelKosmos.kt
new file mode 100644
index 0000000..34a5277
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaRecommendationsInteractor
+import com.android.systemui.media.controls.util.mediaUiEventLogger
+
+val Kosmos.mediaRecommendationsViewModel by
+ Kosmos.Fixture {
+ MediaRecommendationsViewModel(
+ applicationContext = applicationContext,
+ backgroundDispatcher = testDispatcher,
+ interactor = mediaRecommendationsInteractor,
+ logger = mediaUiEventLogger,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index 8109b60..2d5a361 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -31,7 +31,6 @@
override val tableLogBuffer: TableLogBuffer,
) : MobileConnectionRepository {
override val carrierId = MutableStateFlow(UNKNOWN_CARRIER_ID)
- override val inflateSignalStrength: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isEmergencyOnly = MutableStateFlow(false)
override val isRoaming = MutableStateFlow(false)
override val operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null)
diff --git a/services/core/java/com/android/server/am/AccessCheckDelegateHelper.java b/services/core/java/com/android/server/am/AccessCheckDelegateHelper.java
new file mode 100644
index 0000000..62c6329
--- /dev/null
+++ b/services/core/java/com/android/server/am/AccessCheckDelegateHelper.java
@@ -0,0 +1,272 @@
+/*
+ * 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.am;
+
+import android.annotation.Nullable;
+import android.os.Process;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.appop.AppOpsService;
+import com.android.server.pm.permission.AccessCheckDelegate;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+
+import java.util.Collections;
+import java.util.List;
+
+class AccessCheckDelegateHelper {
+ private final ActivityManagerGlobalLock mProcLock;
+
+ @GuardedBy("mProcLock")
+ private final List<ActiveInstrumentation> mActiveInstrumentation;
+
+ private final AppOpsService mAppOpsService;
+
+ private final PermissionManagerServiceInternal mPermissionManagerInternal;
+
+ @GuardedBy("mProcLock")
+ private AccessCheckDelegate mAccessCheckDelegate;
+
+ AccessCheckDelegateHelper(ActivityManagerGlobalLock procLock,
+ List<ActiveInstrumentation> activeInstrumentation, AppOpsService appOpsService,
+ PermissionManagerServiceInternal permissionManagerInternal) {
+ mProcLock = procLock;
+ mActiveInstrumentation = activeInstrumentation;
+ mAppOpsService = appOpsService;
+ mPermissionManagerInternal = permissionManagerInternal;
+ }
+
+ @GuardedBy("mProcLock")
+ private AccessCheckDelegate getAccessCheckDelegateLPr(boolean create) {
+ if (create && mAccessCheckDelegate == null) {
+ mAccessCheckDelegate = new AccessCheckDelegate.AccessCheckDelegateImpl();
+ mAppOpsService.setCheckOpsDelegate(mAccessCheckDelegate);
+ mPermissionManagerInternal.setCheckPermissionDelegate(mAccessCheckDelegate);
+ }
+
+ return mAccessCheckDelegate;
+ }
+
+ @GuardedBy("mProcLock")
+ private void removeAccessCheckDelegateLPr() {
+ mAccessCheckDelegate = null;
+ mAppOpsService.setCheckOpsDelegate(null);
+ mPermissionManagerInternal.setCheckPermissionDelegate(null);
+ }
+
+ void startDelegateShellPermissionIdentity(int delegateUid,
+ @Nullable String[] permissions) {
+ if (UserHandle.getCallingAppId() != Process.SHELL_UID
+ && UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only the shell can delegate its permissions");
+ }
+
+ synchronized (mProcLock) {
+ AccessCheckDelegate delegate = getAccessCheckDelegateLPr(false);
+ if (delegate != null && !delegate.isDelegateAndOwnerUid(delegateUid)) {
+ throw new SecurityException("Shell can delegate permissions only "
+ + "to one instrumentation at a time");
+ }
+ final int instrCount = mActiveInstrumentation.size();
+ for (int i = 0; i < instrCount; i++) {
+ final ActiveInstrumentation instr =
+ mActiveInstrumentation.get(i);
+ if (instr.mTargetInfo.uid != delegateUid) {
+ continue;
+ }
+
+ // If instrumentation started from the shell the connection is not null
+ if (instr.mUiAutomationConnection == null) {
+ throw new SecurityException("Shell can delegate its permissions"
+ + " only to an instrumentation started from the shell");
+ }
+
+ final String packageName = instr.mTargetInfo.packageName;
+ delegate = getAccessCheckDelegateLPr(true);
+ delegate.setShellPermissionDelegate(delegateUid, packageName, permissions);
+ return;
+ }
+ }
+ }
+
+ void stopDelegateShellPermissionIdentity() {
+ if (UserHandle.getCallingAppId() != Process.SHELL_UID
+ && UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only the shell can delegate its permissions");
+ }
+ synchronized (mProcLock) {
+ AccessCheckDelegate delegate = getAccessCheckDelegateLPr(false);
+ if (delegate == null) {
+ return;
+ }
+
+ if (!delegate.hasShellPermissionDelegate()) {
+ return;
+ }
+
+ delegate.removeShellPermissionDelegate();
+
+ if (!delegate.hasDelegateOrOverrides()) {
+ removeAccessCheckDelegateLPr();
+ }
+ }
+ }
+
+ List<String> getDelegatedShellPermissions() {
+ if (UserHandle.getCallingAppId() != Process.SHELL_UID
+ && UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only the shell can get delegated permissions");
+ }
+ synchronized (mProcLock) {
+ AccessCheckDelegate delegate = getAccessCheckDelegateLPr(false);
+ if (delegate == null) {
+ return Collections.EMPTY_LIST;
+ }
+
+ return delegate.getDelegatedPermissionNames();
+ }
+ }
+
+ void addOverridePermissionState(int originatingUid, int uid, String permission, int result) {
+ if (UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only root can override permissions");
+ }
+
+ synchronized (mProcLock) {
+ final int instrCount = mActiveInstrumentation.size();
+ for (int i = 0; i < instrCount; i++) {
+ final ActiveInstrumentation instr =
+ mActiveInstrumentation.get(i);
+ if (instr.mTargetInfo.uid != originatingUid) {
+ continue;
+ }
+ // If instrumentation started from the shell the connection is not null
+ if (instr.mSourceUid != Process.ROOT_UID || instr.mUiAutomationConnection == null) {
+ throw new SecurityException("Root can only override permissions only if the "
+ + "owning app was instrumented from root.");
+ }
+
+ AccessCheckDelegate delegate =
+ getAccessCheckDelegateLPr(true);
+ if (delegate.hasOverriddenPermissions()
+ && !delegate.isDelegateAndOwnerUid(originatingUid)) {
+ throw new SecurityException("Only one instrumentation to grant"
+ + " overrides is allowed at a time.");
+ }
+
+ delegate.addOverridePermissionState(originatingUid, uid, permission, result);
+ return;
+ }
+ }
+ }
+
+ void removeOverridePermissionState(int originatingUid, int uid, String permission) {
+ if (UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only root can override permissions.");
+ }
+
+ synchronized (mProcLock) {
+ AccessCheckDelegate delegate = getAccessCheckDelegateLPr(false);
+ if (delegate == null) {
+ return;
+ }
+
+ if (!delegate.isDelegateAndOwnerUid(originatingUid)) {
+ if (delegate.hasOverriddenPermissions()) {
+ throw new SecurityException("Only the granter of current overrides can remove "
+ + "them.");
+ }
+ return;
+ }
+
+ delegate.removeOverridePermissionState(uid, permission);
+
+ if (!delegate.hasDelegateOrOverrides()) {
+ removeAccessCheckDelegateLPr();
+ }
+ }
+ }
+
+ void clearOverridePermissionStates(int originatingUid, int uid) {
+ if (UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only root can override permissions.");
+ }
+ synchronized (mProcLock) {
+ AccessCheckDelegate delegate = getAccessCheckDelegateLPr(false);
+ if (delegate == null) {
+ return;
+ }
+
+ if (!delegate.isDelegateAndOwnerUid(originatingUid)) {
+ if (delegate.hasOverriddenPermissions()) {
+ throw new SecurityException(
+ "Only the granter of current overrides can remove them.");
+ }
+ return;
+ }
+
+ delegate.clearOverridePermissionStates(uid);
+
+ if (!delegate.hasDelegateOrOverrides()) {
+ removeAccessCheckDelegateLPr();
+ }
+ }
+ }
+
+ void clearAllOverridePermissionStates(int originatingUid) {
+ if (UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only root can override permissions.");
+ }
+ synchronized (mProcLock) {
+ AccessCheckDelegate delegate = getAccessCheckDelegateLPr(false);
+ if (delegate == null) {
+ return;
+ }
+
+ if (!delegate.isDelegateAndOwnerUid(originatingUid)) {
+ if (delegate.hasOverriddenPermissions()) {
+ throw new SecurityException(
+ "Only the granter of current overrides can remove them.");
+ }
+ return;
+ }
+
+ delegate.clearAllOverridePermissionStates();
+
+ if (!delegate.hasDelegateOrOverrides()) {
+ removeAccessCheckDelegateLPr();
+ }
+ }
+ }
+
+ void onInstrumentationFinished(int uid, String packageName) {
+ synchronized (mProcLock) {
+ AccessCheckDelegate delegate = getAccessCheckDelegateLPr(false);
+ if (delegate != null) {
+ if (delegate.isDelegatePackage(uid, packageName)) {
+ delegate.removeShellPermissionDelegate();
+ }
+ if (delegate.isDelegateAndOwnerUid(uid)) {
+ delegate.clearAllOverridePermissionStates();
+ }
+ if (!delegate.hasDelegateOrOverrides()) {
+ removeAccessCheckDelegateLPr();
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 0bc2a91..a66c23f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -51,7 +51,6 @@
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHELL;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SYSTEM_INIT;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UI_VISIBILITY;
-import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.OP_NONE;
import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_BACKUP;
import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_INSTRUMENTATION;
@@ -251,7 +250,6 @@
import android.app.ProcessMemoryState;
import android.app.ProfilerInfo;
import android.app.ServiceStartNotAllowedException;
-import android.app.SyncNotedAppOp;
import android.app.WaitResult;
import android.app.assist.ActivityId;
import android.app.backup.BackupAnnotations.BackupDestination;
@@ -427,17 +425,11 @@
import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
import com.android.internal.policy.AttributeCache;
import com.android.internal.protolog.common.ProtoLog;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastPrintWriter;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.MemInfoReader;
import com.android.internal.util.Preconditions;
-import com.android.internal.util.function.DodecFunction;
-import com.android.internal.util.function.HexFunction;
-import com.android.internal.util.function.OctFunction;
-import com.android.internal.util.function.QuadFunction;
-import com.android.internal.util.function.UndecFunction;
import com.android.server.AlarmManagerInternal;
import com.android.server.BootReceiver;
import com.android.server.DeviceIdleInternal;
@@ -770,6 +762,8 @@
@GuardedBy("mDeliveryGroupPolicyIgnoredActions")
private final ArraySet<String> mDeliveryGroupPolicyIgnoredActions = new ArraySet();
+ private AccessCheckDelegateHelper mAccessCheckDelegateHelper;
+
/**
* Uids of apps with current active camera sessions. Access synchronized on
* the IntArray instance itself, and no other locks must be acquired while that
@@ -6937,6 +6931,16 @@
return mPermissionManagerInt;
}
+ private AccessCheckDelegateHelper getAccessCheckDelegateHelper() {
+ // Intentionally hold no locks: in case of race conditions, the mPermissionManagerInt will
+ // be set to the same value anyway.
+ if (mAccessCheckDelegateHelper == null) {
+ mAccessCheckDelegateHelper = new AccessCheckDelegateHelper(mProcLock,
+ mActiveInstrumentation, mAppOpsService, getPermissionManagerInternal());
+ }
+ return mAccessCheckDelegateHelper;
+ }
+
/** Returns whether the given package was ever launched since install */
boolean wasPackageEverLaunched(String packageName, @UserIdInt int userId) {
boolean wasLaunched = false;
@@ -16598,8 +16602,8 @@
// Go back to the default mode of denying OP_NO_ISOLATED_STORAGE app op.
mAppOpsService.setMode(AppOpsManager.OP_NO_ISOLATED_STORAGE, app.uid,
app.info.packageName, AppOpsManager.MODE_ERRORED);
- mAppOpsService.setAppOpsServiceDelegate(null);
- getPermissionManagerInternal().stopShellPermissionIdentityDelegation();
+ getAccessCheckDelegateHelper()
+ .onInstrumentationFinished(app.uid, app.info.packageName);
mHandler.obtainMessage(SHUTDOWN_UI_AUTOMATION_CONNECTION_MSG,
instr.mUiAutomationConnection).sendToTarget();
}
@@ -18122,7 +18126,8 @@
public ComponentName startSdkSandboxService(Intent service, int clientAppUid,
String clientAppPackage, String processName) throws RemoteException {
validateSdkSandboxParams(service, clientAppUid, clientAppPackage, processName);
- if (mAppOpsService.checkPackage(clientAppUid, clientAppPackage) != MODE_ALLOWED) {
+ if (mAppOpsService.checkPackage(clientAppUid, clientAppPackage)
+ != AppOpsManager.MODE_ALLOWED) {
throw new IllegalArgumentException("uid does not belong to provided package");
}
// TODO(b/269598719): Is passing the application thread of the system_server alright?
@@ -18191,7 +18196,8 @@
String processName, long flags)
throws RemoteException {
validateSdkSandboxParams(service, clientAppUid, clientAppPackage, processName);
- if (mAppOpsService.checkPackage(clientAppUid, clientAppPackage) != MODE_ALLOWED) {
+ if (mAppOpsService.checkPackage(clientAppUid, clientAppPackage)
+ != AppOpsManager.MODE_ALLOWED) {
throw new IllegalArgumentException("uid does not belong to provided package");
}
if (conn == null) {
@@ -20499,268 +20505,41 @@
@Override
public void startDelegateShellPermissionIdentity(int delegateUid,
@Nullable String[] permissions) {
- if (UserHandle.getCallingAppId() != Process.SHELL_UID
- && UserHandle.getCallingAppId() != Process.ROOT_UID) {
- throw new SecurityException("Only the shell can delegate its permissions");
- }
-
- // We allow delegation only to one instrumentation started from the shell
- synchronized (mProcLock) {
- // If the delegate is already set up for the target UID, nothing to do.
- if (mAppOpsService.getAppOpsServiceDelegate() != null) {
- if (!(mAppOpsService.getAppOpsServiceDelegate() instanceof ShellDelegate)) {
- throw new IllegalStateException("Bad shell delegate state");
- }
- final ShellDelegate delegate = (ShellDelegate) mAppOpsService
- .getAppOpsServiceDelegate();
- if (delegate.getDelegateUid() != delegateUid) {
- throw new SecurityException("Shell can delegate permissions only "
- + "to one instrumentation at a time");
- }
- }
-
- final int instrCount = mActiveInstrumentation.size();
- for (int i = 0; i < instrCount; i++) {
- final ActiveInstrumentation instr = mActiveInstrumentation.get(i);
- if (instr.mTargetInfo.uid != delegateUid) {
- continue;
- }
- // If instrumentation started from the shell the connection is not null
- if (instr.mUiAutomationConnection == null) {
- throw new SecurityException("Shell can delegate its permissions" +
- " only to an instrumentation started from the shell");
- }
-
- // Hook them up...
- final ShellDelegate shellDelegate = new ShellDelegate(delegateUid,
- permissions);
- mAppOpsService.setAppOpsServiceDelegate(shellDelegate);
- final String packageName = instr.mTargetInfo.packageName;
- final List<String> permissionNames = permissions != null ?
- Arrays.asList(permissions) : null;
- getPermissionManagerInternal().startShellPermissionIdentityDelegation(
- delegateUid, packageName, permissionNames);
- return;
- }
- }
+ getAccessCheckDelegateHelper()
+ .startDelegateShellPermissionIdentity(delegateUid, permissions);
}
@Override
public void stopDelegateShellPermissionIdentity() {
- if (UserHandle.getCallingAppId() != Process.SHELL_UID
- && UserHandle.getCallingAppId() != Process.ROOT_UID) {
- throw new SecurityException("Only the shell can delegate its permissions");
- }
- synchronized (mProcLock) {
- mAppOpsService.setAppOpsServiceDelegate(null);
- getPermissionManagerInternal().stopShellPermissionIdentityDelegation();
- }
+ getAccessCheckDelegateHelper().stopDelegateShellPermissionIdentity();
}
@Override
public List<String> getDelegatedShellPermissions() {
- if (UserHandle.getCallingAppId() != Process.SHELL_UID
- && UserHandle.getCallingAppId() != Process.ROOT_UID) {
- throw new SecurityException("Only the shell can get delegated permissions");
- }
- synchronized (mProcLock) {
- return getPermissionManagerInternal().getDelegatedShellPermissions();
- }
+ return getAccessCheckDelegateHelper().getDelegatedShellPermissions();
}
- private class ShellDelegate implements CheckOpsDelegate {
- private final int mTargetUid;
- @Nullable
- private final String[] mPermissions;
+ @Override
+ public void addOverridePermissionState(int originatingUid, int uid, String permission,
+ int result) {
+ getAccessCheckDelegateHelper()
+ .addOverridePermissionState(originatingUid, uid, permission, result);
+ }
- ShellDelegate(int targetUid, @Nullable String[] permissions) {
- mTargetUid = targetUid;
- mPermissions = permissions;
- }
+ @Override
+ public void removeOverridePermissionState(int originatingUid, int uid, String permission) {
+ getAccessCheckDelegateHelper()
+ .removeOverridePermissionState(originatingUid, uid, permission);
+ }
- int getDelegateUid() {
- return mTargetUid;
- }
+ @Override
+ public void clearOverridePermissionStates(int originatingUid, int uid) {
+ getAccessCheckDelegateHelper().clearOverridePermissionStates(originatingUid, uid);
+ }
- @Override
- public int checkOperation(int code, int uid, String packageName, String attributionTag,
- int virtualDeviceId, boolean raw, HexFunction<Integer, Integer, String, String,
- Integer, Boolean, Integer> superImpl) {
- if (uid == mTargetUid && isTargetOp(code)) {
- final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
- Process.SHELL_UID);
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply(code, shellUid, "com.android.shell", null,
- virtualDeviceId, raw);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(code, uid, packageName, attributionTag, virtualDeviceId, raw);
- }
-
- @Override
- public int checkAudioOperation(int code, int usage, int uid, String packageName,
- QuadFunction<Integer, Integer, Integer, String, Integer> superImpl) {
- if (uid == mTargetUid && isTargetOp(code)) {
- final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
- Process.SHELL_UID);
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply(code, usage, shellUid, "com.android.shell");
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(code, usage, uid, packageName);
- }
-
- @Override
- public SyncNotedAppOp noteOperation(int code, int uid, @Nullable String packageName,
- @Nullable String featureId, int virtualDeviceId, boolean shouldCollectAsyncNotedOp,
- @Nullable String message, boolean shouldCollectMessage,
- @NonNull OctFunction<Integer, Integer, String, String, Integer, Boolean, String,
- Boolean, SyncNotedAppOp> superImpl) {
- if (uid == mTargetUid && isTargetOp(code)) {
- final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
- Process.SHELL_UID);
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply(code, shellUid, "com.android.shell", featureId,
- virtualDeviceId, shouldCollectAsyncNotedOp, message,
- shouldCollectMessage);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(code, uid, packageName, featureId, virtualDeviceId,
- shouldCollectAsyncNotedOp, message, shouldCollectMessage);
- }
-
- @Override
- public SyncNotedAppOp noteProxyOperation(int code,
- @NonNull AttributionSource attributionSource, boolean shouldCollectAsyncNotedOp,
- @Nullable String message, boolean shouldCollectMessage, boolean skiProxyOperation,
- @NonNull HexFunction<Integer, AttributionSource, Boolean, String, Boolean,
- Boolean, SyncNotedAppOp> superImpl) {
- if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) {
- final int shellUid = UserHandle.getUid(UserHandle.getUserId(
- attributionSource.getUid()), Process.SHELL_UID);
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply(code, new AttributionSource(shellUid,
- Process.INVALID_PID, "com.android.shell",
- attributionSource.getAttributionTag(), attributionSource.getToken(),
- /*renouncedPermissions*/ null, attributionSource.getDeviceId(),
- attributionSource.getNext()),
- shouldCollectAsyncNotedOp, message, shouldCollectMessage,
- skiProxyOperation);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(code, attributionSource, shouldCollectAsyncNotedOp,
- message, shouldCollectMessage, skiProxyOperation);
- }
-
- @Override
- public SyncNotedAppOp startOperation(IBinder token, int code, int uid,
- @Nullable String packageName, @Nullable String attributionTag, int virtualDeviceId,
- boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp,
- @Nullable String message, boolean shouldCollectMessage,
- @AttributionFlags int attributionFlags, int attributionChainId,
- @NonNull DodecFunction<IBinder, Integer, Integer, String, String, Integer, Boolean,
- Boolean, String, Boolean, Integer, Integer, SyncNotedAppOp> superImpl) {
- if (uid == mTargetUid && isTargetOp(code)) {
- final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
- Process.SHELL_UID);
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply(token, code, shellUid, "com.android.shell",
- attributionTag, virtualDeviceId, startIfModeDefault,
- shouldCollectAsyncNotedOp, message, shouldCollectMessage,
- attributionFlags, attributionChainId);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(token, code, uid, packageName, attributionTag, virtualDeviceId,
- startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
- attributionFlags, attributionChainId);
- }
-
- @Override
- public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
- @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
- boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
- boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
- @AttributionFlags int proxiedAttributionFlags, int attributionChainId,
- @NonNull UndecFunction<IBinder, Integer, AttributionSource,
- Boolean, Boolean, String, Boolean, Boolean, Integer, Integer, Integer,
- SyncNotedAppOp> superImpl) {
- if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) {
- final int shellUid = UserHandle.getUid(UserHandle.getUserId(
- attributionSource.getUid()), Process.SHELL_UID);
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply(clientId, code, new AttributionSource(shellUid,
- Process.INVALID_PID, "com.android.shell",
- attributionSource.getAttributionTag(), attributionSource.getToken(),
- /*renouncedPermissions*/ null, attributionSource.getDeviceId(),
- attributionSource.getNext()),
- startIfModeDefault, shouldCollectAsyncNotedOp, message,
- shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
- proxiedAttributionFlags, attributionChainId);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(clientId, code, attributionSource, startIfModeDefault,
- shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation,
- proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
- }
-
- @Override
- public void finishProxyOperation(@NonNull IBinder clientId, int code,
- @NonNull AttributionSource attributionSource, boolean skipProxyOperation,
- @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean,
- Void> superImpl) {
- if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) {
- final int shellUid = UserHandle.getUid(UserHandle.getUserId(
- attributionSource.getUid()), Process.SHELL_UID);
- final long identity = Binder.clearCallingIdentity();
- try {
- superImpl.apply(clientId, code, new AttributionSource(shellUid,
- Process.INVALID_PID, "com.android.shell",
- attributionSource.getAttributionTag(), attributionSource.getToken(),
- /*renouncedPermissions*/ null, attributionSource.getDeviceId(),
- attributionSource.getNext()),
- skipProxyOperation);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- superImpl.apply(clientId, code, attributionSource, skipProxyOperation);
- }
-
- private boolean isTargetOp(int code) {
- // null permissions means all ops are targeted
- if (mPermissions == null) {
- return true;
- }
- // no permission for the op means the op is targeted
- final String permission = AppOpsManager.opToPermission(code);
- if (permission == null) {
- return true;
- }
- return isTargetPermission(permission);
- }
-
- private boolean isTargetPermission(@NonNull String permission) {
- // null permissions means all permissions are targeted
- return (mPermissions == null || ArrayUtils.contains(mPermissions, permission));
- }
+ @Override
+ public void clearAllOverridePermissionStates(int originatingUid) {
+ getAccessCheckDelegateHelper().clearAllOverridePermissionStates(originatingUid);
}
/**
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index bf7cc10..a656287 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -18,6 +18,7 @@
# Permissions & Packages
patb@google.com
+per-file AccessCheckDelegateHelper.java = file:/core/java/android/permission/OWNERS
# Battery Stats
joeo@google.com
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index fb62785..debd9d0 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2671,14 +2671,10 @@
}
}
- public CheckOpsDelegate getAppOpsServiceDelegate() {
- synchronized (AppOpsService.this) {
- final CheckOpsDelegateDispatcher dispatcher = mCheckOpsDelegateDispatcher;
- return (dispatcher != null) ? dispatcher.getCheckOpsDelegate() : null;
- }
- }
-
- public void setAppOpsServiceDelegate(CheckOpsDelegate delegate) {
+ /**
+ * Sets the CheckOpDelegate
+ */
+ public void setCheckOpsDelegate(CheckOpsDelegate delegate) {
synchronized (AppOpsService.this) {
final CheckOpsDelegateDispatcher oldDispatcher = mCheckOpsDelegateDispatcher;
final CheckOpsDelegate policy = (oldDispatcher != null) ? oldDispatcher.mPolicy : null;
@@ -7157,10 +7153,6 @@
mCheckOpsDelegate = checkOpsDelegate;
}
- public @NonNull CheckOpsDelegate getCheckOpsDelegate() {
- return mCheckOpsDelegate;
- }
-
public int checkOperation(int code, int uid, String packageName,
@Nullable String attributionTag, int virtualDeviceId, boolean raw) {
if (mPolicy != null) {
diff --git a/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java b/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java
new file mode 100644
index 0000000..3edd697
--- /dev/null
+++ b/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java
@@ -0,0 +1,499 @@
+/*
+ * 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.pm.permission;
+
+import static android.os.Process.INVALID_UID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.AttributionFlags;
+import android.app.AppOpsManagerInternal.CheckOpsDelegate;
+import android.app.SyncNotedAppOp;
+import android.content.AttributionSource;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.function.DodecFunction;
+import com.android.internal.util.function.HexFunction;
+import com.android.internal.util.function.OctFunction;
+import com.android.internal.util.function.QuadFunction;
+import com.android.internal.util.function.TriFunction;
+import com.android.internal.util.function.UndecFunction;
+import com.android.server.LocalServices;
+import com.android.server.pm.permission.PermissionManagerServiceInternal.CheckPermissionDelegate;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface to intercept incoming parameters and outgoing results to permission and appop checks
+ */
+public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDelegate {
+
+ /**
+ * Assigns the package whose permissions are delegated the state of Shell's permissions.
+ *
+ * @param delegateUid the UID whose permissions are delegated to shell
+ * @param packageName the name of the package whose permissions are delegated to shell
+ * @param permissions the set of permissions to delegate to shell. If null then all
+ * permission will be delegated
+ */
+ void setShellPermissionDelegate(int delegateUid, @NonNull String packageName,
+ @Nullable String[] permissions);
+
+ /**
+ * Removes the assigned Shell permission delegate.
+ */
+ void removeShellPermissionDelegate();
+
+ /**
+ * @return a list of permissions delegated to Shell's permission state
+ */
+ @NonNull
+ List<String> getDelegatedPermissionNames();
+
+ /**
+ * @return whether there exists a Shell permission delegate
+ */
+ boolean hasShellPermissionDelegate();
+
+ /**
+ * @param uid the UID to check
+ * @return whether the UID's permissions are delegated to Shell's and the owner of overrides
+ */
+ boolean isDelegateAndOwnerUid(int uid);
+
+ /**
+ * @param uid the UID to check
+ * @param packageName the package to check
+ * @return whether the UID and package combination's permissions are delegated to Shell's
+ * permissions
+ */
+ boolean isDelegatePackage(int uid, @NonNull String packageName);
+
+ /**
+ * Adds permission to be overridden to the given state.
+ *
+ * @param ownerUid the UID of the app who assigned the permission override
+ * @param uid The UID of the app whose permission will be overridden
+ * @param permission The permission whose state will be overridden
+ * @param result The state to override the permission to
+ */
+ void addOverridePermissionState(int ownerUid, int uid, @NonNull String permission,
+ int result);
+
+ /**
+ * Removes overridden permission. UiAutomation must be connected to root user.
+ *
+ * @param uid The UID of the app whose permission is overridden
+ * @param permission The permission whose state will no longer be overridden
+ *
+ * @hide
+ */
+ void removeOverridePermissionState(int uid, @NonNull String permission);
+
+ /**
+ * Clears all overridden permissions for the given UID.
+ *
+ * @param uid The UID of the app whose permissions will no longer be overridden
+ */
+ void clearOverridePermissionStates(int uid);
+
+ /**
+ * Clears all overridden permissions on the device.
+ */
+ void clearAllOverridePermissionStates();
+
+ /**
+ * @return whether there exists any permission overrides
+ */
+ boolean hasOverriddenPermissions();
+
+ /**
+ * @return whether there exists permissions delegated to Shell's permissions or overridden
+ */
+ boolean hasDelegateOrOverrides();
+
+ class AccessCheckDelegateImpl implements AccessCheckDelegate {
+ public static final String SHELL_PKG = "com.android.shell";
+ private int mDelegateAndOwnerUid = INVALID_UID;
+ @Nullable
+ private String mDelegatePackage;
+ @Nullable
+ private String[] mDelegatePermissions;
+ boolean mDelegateAllPermissions;
+ @Nullable
+ private SparseArray<ArrayMap<String, Integer>> mOverridePermissionStates;
+
+ @Override
+ public void setShellPermissionDelegate(int uid, @NonNull String packageName,
+ @Nullable String[] permissions) {
+ mDelegateAndOwnerUid = uid;
+ mDelegatePackage = packageName;
+ mDelegatePermissions = permissions;
+ mDelegateAllPermissions = permissions == null;
+ PackageManager.invalidatePackageInfoCache();
+ }
+
+ @Override
+ public void removeShellPermissionDelegate() {
+ mDelegatePackage = null;
+ mDelegatePermissions = null;
+ mDelegateAllPermissions = false;
+ PackageManager.invalidatePackageInfoCache();
+ }
+
+ @Override
+ public void addOverridePermissionState(int ownerUid, int uid, @NonNull String permission,
+ int state) {
+ if (mOverridePermissionStates == null) {
+ mDelegateAndOwnerUid = ownerUid;
+ mOverridePermissionStates = new SparseArray<>();
+ }
+
+ int uidIdx = mOverridePermissionStates.indexOfKey(uid);
+ ArrayMap<String, Integer> perUidOverrides;
+ if (uidIdx < 0) {
+ perUidOverrides = new ArrayMap<>();
+ mOverridePermissionStates.put(uid, perUidOverrides);
+ } else {
+ perUidOverrides = mOverridePermissionStates.valueAt(uidIdx);
+ }
+
+ perUidOverrides.put(permission, state);
+ PackageManager.invalidatePackageInfoCache();
+ }
+
+ @Override
+ public void removeOverridePermissionState(int uid, @NonNull String permission) {
+ if (mOverridePermissionStates == null) {
+ return;
+ }
+
+ ArrayMap<String, Integer> perUidOverrides = mOverridePermissionStates.get(uid);
+
+ if (perUidOverrides == null) {
+ return;
+ }
+
+ perUidOverrides.remove(permission);
+ PackageManager.invalidatePackageInfoCache();
+
+ if (perUidOverrides.isEmpty()) {
+ mOverridePermissionStates.remove(uid);
+ }
+ if (mOverridePermissionStates.size() == 0) {
+ mOverridePermissionStates = null;
+ }
+ }
+
+ @Override
+ public void clearOverridePermissionStates(int uid) {
+ if (mOverridePermissionStates == null) {
+ return;
+ }
+
+ mOverridePermissionStates.remove(uid);
+ PackageManager.invalidatePackageInfoCache();
+
+ if (mOverridePermissionStates.size() == 0) {
+ mOverridePermissionStates = null;
+ }
+ }
+
+ @Override
+ public void clearAllOverridePermissionStates() {
+ mOverridePermissionStates = null;
+ PackageManager.invalidatePackageInfoCache();
+ }
+
+ @Override
+ public List<String> getDelegatedPermissionNames() {
+ return mDelegatePermissions == null ? null : List.of(mDelegatePermissions);
+ }
+
+ @Override
+ public boolean hasShellPermissionDelegate() {
+ return mDelegateAllPermissions || mDelegatePermissions != null;
+ }
+
+ @Override
+ public boolean isDelegatePackage(int uid, @NonNull String packageName) {
+ return mDelegateAndOwnerUid == uid && TextUtils.equals(mDelegatePackage, packageName);
+ }
+
+ @Override
+ public boolean hasOverriddenPermissions() {
+ return mOverridePermissionStates != null;
+ }
+
+ @Override
+ public boolean isDelegateAndOwnerUid(int uid) {
+ return uid == mDelegateAndOwnerUid;
+ }
+
+ @Override
+ public boolean hasDelegateOrOverrides() {
+ return hasShellPermissionDelegate() || hasOverriddenPermissions();
+ }
+
+ @Override
+ public int checkPermission(@NonNull String packageName, @NonNull String permissionName,
+ @NonNull String persistentDeviceId, @UserIdInt int userId,
+ @NonNull QuadFunction<String, String, String, Integer, Integer> superImpl) {
+ if (TextUtils.equals(mDelegatePackage, packageName) && !SHELL_PKG.equals(packageName)) {
+ if (isDelegatePermission(permissionName)) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return checkPermission(SHELL_PKG, permissionName,
+ persistentDeviceId, userId, superImpl);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+ if (mOverridePermissionStates != null) {
+ int uid = LocalServices.getService(PackageManagerInternal.class)
+ .getPackageUid(packageName, 0, userId);
+ if (uid >= 0) {
+ Map<String, Integer> permissionGrants = mOverridePermissionStates.get(uid);
+ if (permissionGrants != null && permissionGrants.containsKey(permissionName)) {
+ return permissionGrants.get(permissionName);
+ }
+ }
+ }
+ return superImpl.apply(packageName, permissionName, persistentDeviceId, userId);
+ }
+
+ @Override
+ public int checkUidPermission(int uid, @NonNull String permissionName,
+ @NonNull String persistentDeviceId,
+ @NonNull TriFunction<Integer, String, String, Integer> superImpl) {
+ if (uid == mDelegateAndOwnerUid && uid != Process.SHELL_UID) {
+ if (isDelegatePermission(permissionName)) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return checkUidPermission(Process.SHELL_UID, permissionName,
+ persistentDeviceId, superImpl);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+ if (mOverridePermissionStates != null) {
+ Map<String, Integer> permissionGrants = mOverridePermissionStates.get(uid);
+ if (permissionGrants != null && permissionGrants.containsKey(permissionName)) {
+ return permissionGrants.get(permissionName);
+ }
+ }
+ return superImpl.apply(uid, permissionName, persistentDeviceId);
+ }
+
+ @Override
+ public int checkOperation(int code, int uid, @Nullable String packageName,
+ @Nullable String attributionTag, int virtualDeviceId, boolean raw,
+ @NonNull HexFunction<Integer, Integer, String, String, Integer, Boolean, Integer>
+ superImpl) {
+ if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) {
+ final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
+ Process.SHELL_UID);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(code, shellUid, "com.android.shell", null,
+ virtualDeviceId, raw);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(code, uid, packageName, attributionTag, virtualDeviceId, raw);
+ }
+
+ @Override
+ public int checkAudioOperation(int code, int usage, int uid, @Nullable String packageName,
+ @NonNull QuadFunction<Integer, Integer, Integer, String, Integer> superImpl) {
+ if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) {
+ final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
+ Process.SHELL_UID);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(code, usage, shellUid, "com.android.shell");
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(code, usage, uid, packageName);
+ }
+
+ @Override
+ public SyncNotedAppOp noteOperation(int code, int uid, @Nullable String packageName,
+ @Nullable String featureId, int virtualDeviceId, boolean shouldCollectAsyncNotedOp,
+ @Nullable String message, boolean shouldCollectMessage,
+ @NonNull OctFunction<Integer, Integer, String, String, Integer, Boolean, String,
+ Boolean, SyncNotedAppOp> superImpl) {
+ if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) {
+ final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
+ Process.SHELL_UID);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(code, shellUid, "com.android.shell", featureId,
+ virtualDeviceId, shouldCollectAsyncNotedOp, message,
+ shouldCollectMessage);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(code, uid, packageName, featureId, virtualDeviceId,
+ shouldCollectAsyncNotedOp, message, shouldCollectMessage);
+ }
+
+ @Override
+ public SyncNotedAppOp noteProxyOperation(int code,
+ @NonNull AttributionSource attributionSource, boolean shouldCollectAsyncNotedOp,
+ @Nullable String message, boolean shouldCollectMessage, boolean skiProxyOperation,
+ @NonNull HexFunction<Integer, AttributionSource, Boolean, String, Boolean,
+ Boolean, SyncNotedAppOp> superImpl) {
+ if (attributionSource.getUid() == mDelegateAndOwnerUid && isDelegateOp(code)) {
+ final int shellUid = UserHandle.getUid(
+ UserHandle.getUserId(attributionSource.getUid()), Process.SHELL_UID);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(code,
+ new AttributionSource(shellUid, Process.INVALID_PID,
+ "com.android.shell", attributionSource.getAttributionTag(),
+ attributionSource.getToken(), /*renouncedPermissions*/ null,
+ attributionSource.getDeviceId(), attributionSource.getNext()),
+ shouldCollectAsyncNotedOp, message, shouldCollectMessage,
+ skiProxyOperation);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(code, attributionSource, shouldCollectAsyncNotedOp,
+ message, shouldCollectMessage, skiProxyOperation);
+ }
+
+ @Override
+ public SyncNotedAppOp startOperation(@NonNull IBinder token, int code, int uid,
+ @Nullable String packageName, @Nullable String attributionTag, int virtualDeviceId,
+ boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp,
+ @Nullable String message, boolean shouldCollectMessage,
+ @AttributionFlags int attributionFlags, int attributionChainId,
+ @NonNull DodecFunction<IBinder, Integer, Integer, String, String, Integer, Boolean,
+ Boolean, String, Boolean, Integer, Integer, SyncNotedAppOp> superImpl) {
+ if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) {
+ final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid),
+ Process.SHELL_UID);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(token, code, shellUid, "com.android.shell",
+ attributionTag, virtualDeviceId, startIfModeDefault,
+ shouldCollectAsyncNotedOp, message, shouldCollectMessage,
+ attributionFlags, attributionChainId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(token, code, uid, packageName, attributionTag, virtualDeviceId,
+ startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
+ attributionFlags, attributionChainId);
+ }
+
+ @Override
+ public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
+ @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
+ boolean shouldCollectAsyncNotedOp, @Nullable String message,
+ boolean shouldCollectMessage, boolean skipProxyOperation,
+ @AttributionFlags int proxyAttributionFlags,
+ @AttributionFlags int proxiedAttributionFlags, int attributionChainId,
+ @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean,
+ Boolean, String, Boolean, Boolean, Integer, Integer, Integer,
+ SyncNotedAppOp> superImpl) {
+ if (attributionSource.getUid() == mDelegateAndOwnerUid && isDelegateOp(code)) {
+ final int shellUid = UserHandle.getUid(UserHandle.getUserId(
+ attributionSource.getUid()), Process.SHELL_UID);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(clientId, code,
+ new AttributionSource(shellUid, Process.INVALID_PID,
+ "com.android.shell", attributionSource.getAttributionTag(),
+ attributionSource.getToken(), /*renouncedPermissions*/ null,
+ attributionSource.getDeviceId(), attributionSource.getNext()),
+ startIfModeDefault, shouldCollectAsyncNotedOp, message,
+ shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
+ proxiedAttributionFlags, attributionChainId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(clientId, code, attributionSource, startIfModeDefault,
+ shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation,
+ proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
+ }
+
+ @Override
+ public void finishProxyOperation(@NonNull IBinder clientId, int code,
+ @NonNull AttributionSource attributionSource, boolean skipProxyOperation,
+ @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean,
+ Void> superImpl) {
+ if (attributionSource.getUid() == mDelegateAndOwnerUid && isDelegateOp(code)) {
+ final int shellUid = UserHandle.getUid(UserHandle.getUserId(
+ attributionSource.getUid()), Process.SHELL_UID);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ superImpl.apply(clientId, code,
+ new AttributionSource(shellUid, Process.INVALID_PID,
+ "com.android.shell", attributionSource.getAttributionTag(),
+ attributionSource.getToken(), /*renouncedPermissions*/ null,
+ attributionSource.getDeviceId(), attributionSource.getNext()),
+ skipProxyOperation);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ superImpl.apply(clientId, code, attributionSource, skipProxyOperation);
+ }
+
+ private boolean isDelegatePermission(@NonNull String permission) {
+ // null permissions means all permissions are delegated
+ return mDelegateAndOwnerUid != INVALID_UID
+ && (mDelegateAllPermissions
+ || ArrayUtils.contains(mDelegatePermissions, permission));
+ }
+
+ private boolean isDelegateOp(int code) {
+ if (mDelegateAllPermissions) {
+ return true;
+ }
+ // no permission for the op means the op is targeted
+ final String permission = AppOpsManager.opToPermission(code);
+ if (permission == null) {
+ return true;
+ }
+ return isDelegatePermission(permission);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 21e2bf2..bd0501d 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -76,11 +76,10 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
-import com.android.internal.util.function.QuadFunction;
-import com.android.internal.util.function.TriFunction;
import com.android.server.LocalServices;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.pm.UserManagerService;
+import com.android.server.pm.permission.PermissionManagerServiceInternal.CheckPermissionDelegate;
import com.android.server.pm.permission.PermissionManagerServiceInternal.HotwordDetectionServiceProvider;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
@@ -88,7 +87,6 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@@ -323,6 +321,12 @@
return true;
}
+ private void setCheckPermissionDelegateInternal(CheckPermissionDelegate delegate) {
+ synchronized (mLock) {
+ mCheckPermissionDelegate = delegate;
+ }
+ }
+
private boolean checkAutoRevokeAccess(AndroidPackage pkg, int callingUid) {
final boolean isCallerPrivileged = mContext.checkCallingOrSelfPermission(
Manifest.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS)
@@ -369,42 +373,6 @@
}
}
- private void startShellPermissionIdentityDelegationInternal(int uid,
- @NonNull String packageName, @Nullable List<String> permissionNames) {
- synchronized (mLock) {
- final CheckPermissionDelegate oldDelegate = mCheckPermissionDelegate;
- if (oldDelegate != null && oldDelegate.getDelegatedUid() != uid) {
- throw new SecurityException(
- "Shell can delegate permissions only to one UID at a time");
- }
- final ShellDelegate delegate = new ShellDelegate(uid, packageName, permissionNames);
- setCheckPermissionDelegateLocked(delegate);
- }
- }
-
- private void stopShellPermissionIdentityDelegationInternal() {
- synchronized (mLock) {
- setCheckPermissionDelegateLocked(null);
- }
- }
-
- @Nullable
- private List<String> getDelegatedShellPermissionsInternal() {
- synchronized (mLock) {
- if (mCheckPermissionDelegate == null) {
- return Collections.EMPTY_LIST;
- }
- return mCheckPermissionDelegate.getDelegatedPermissionNames();
- }
- }
-
- private void setCheckPermissionDelegateLocked(@Nullable CheckPermissionDelegate delegate) {
- if (delegate != null || mCheckPermissionDelegate != null) {
- PackageManager.invalidatePackageInfoCache();
- }
- mCheckPermissionDelegate = delegate;
- }
-
@NonNull
private OneTimePermissionUserManager getOneTimePermissionUserManager(@UserIdInt int userId) {
OneTimePermissionUserManager oneTimePermissionUserManager;
@@ -663,24 +631,6 @@
}
@Override
- public void startShellPermissionIdentityDelegation(int uid, @NonNull String packageName,
- @Nullable List<String> permissionNames) {
- Objects.requireNonNull(packageName, "packageName");
- startShellPermissionIdentityDelegationInternal(uid, packageName, permissionNames);
- }
-
- @Override
- public void stopShellPermissionIdentityDelegation() {
- stopShellPermissionIdentityDelegationInternal();
- }
-
- @Override
- @NonNull
- public List<String> getDelegatedShellPermissions() {
- return getDelegatedShellPermissionsInternal();
- }
-
- @Override
public void setHotwordDetectionServiceProvider(HotwordDetectionServiceProvider provider) {
mHotwordDetectionServiceProvider = provider;
}
@@ -891,6 +841,11 @@
.getAllPermissionsWithProtectionFlags(protectionFlags);
}
+ @Override
+ public void setCheckPermissionDelegate(CheckPermissionDelegate delegate) {
+ setCheckPermissionDelegateInternal(delegate);
+ }
+
/* End of delegate methods to PermissionManagerServiceInterface */
}
@@ -902,120 +857,6 @@
private int[] getAllUserIds() {
return UserManagerService.getInstance().getUserIdsIncludingPreCreated();
}
-
- /**
- * Interface to intercept permission checks and optionally pass through to the original
- * implementation.
- */
- private interface CheckPermissionDelegate {
- /**
- * Get the UID whose permission checks is being delegated.
- *
- * @return the UID
- */
- int getDelegatedUid();
-
- /**
- * Check whether the given package has been granted the specified permission.
- *
- * @param packageName the name of the package to be checked
- * @param permissionName the name of the permission to be checked
- * @param persistentDeviceId The persistent device ID
- * @param userId the user ID
- * @param superImpl the original implementation that can be delegated to
- * @return {@link android.content.pm.PackageManager#PERMISSION_GRANTED} if the package has
- * the permission, or {@link android.content.pm.PackageManager#PERMISSION_DENIED} otherwise
- *
- * @see android.content.pm.PackageManager#checkPermission(String, String)
- */
- int checkPermission(@NonNull String packageName, @NonNull String permissionName,
- String persistentDeviceId, @UserIdInt int userId,
- @NonNull QuadFunction<String, String, String, Integer, Integer> superImpl);
-
- /**
- * Check whether the given UID has been granted the specified permission.
- *
- * @param uid the UID to be checked
- * @param permissionName the name of the permission to be checked
- * @param persistentDeviceId The persistent device ID
- * @param superImpl the original implementation that can be delegated to
- * @return {@link android.content.pm.PackageManager#PERMISSION_GRANTED} if the package has
- * the permission, or {@link android.content.pm.PackageManager#PERMISSION_DENIED} otherwise
- */
- int checkUidPermission(int uid, @NonNull String permissionName, String persistentDeviceId,
- TriFunction<Integer, String, String, Integer> superImpl);
-
- /**
- * @return list of delegated permissions
- */
- List<String> getDelegatedPermissionNames();
- }
-
- private class ShellDelegate implements CheckPermissionDelegate {
- private final int mDelegatedUid;
- @NonNull
- private final String mDelegatedPackageName;
- @Nullable
- private final List<String> mDelegatedPermissionNames;
-
- public ShellDelegate(int delegatedUid, @NonNull String delegatedPackageName,
- @Nullable List<String> delegatedPermissionNames) {
- mDelegatedUid = delegatedUid;
- mDelegatedPackageName = delegatedPackageName;
- mDelegatedPermissionNames = delegatedPermissionNames;
- }
-
- @Override
- public int getDelegatedUid() {
- return mDelegatedUid;
- }
-
- @Override
- public int checkPermission(@NonNull String packageName, @NonNull String permissionName,
- String persistentDeviceId, int userId,
- @NonNull QuadFunction<String, String, String, Integer, Integer> superImpl) {
- if (mDelegatedPackageName.equals(packageName)
- && isDelegatedPermission(permissionName)) {
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply("com.android.shell", permissionName, persistentDeviceId,
- userId);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(packageName, permissionName, persistentDeviceId, userId);
- }
-
- @Override
- public int checkUidPermission(int uid, @NonNull String permissionName,
- String persistentDeviceId,
- @NonNull TriFunction<Integer, String, String, Integer> superImpl) {
- if (uid == mDelegatedUid && isDelegatedPermission(permissionName)) {
- final long identity = Binder.clearCallingIdentity();
- try {
- return superImpl.apply(Process.SHELL_UID, permissionName, persistentDeviceId);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- return superImpl.apply(uid, permissionName, persistentDeviceId);
- }
-
- @Override
- public List<String> getDelegatedPermissionNames() {
- return mDelegatedPermissionNames == null
- ? null
- : new ArrayList<>(mDelegatedPermissionNames);
- }
-
- private boolean isDelegatedPermission(@NonNull String permissionName) {
- // null permissions means all permissions are targeted
- return mDelegatedPermissionNames == null
- || mDelegatedPermissionNames.contains(permissionName);
- }
- }
-
private static final class AttributionSourceRegistry {
private final Object mLock = new Object();
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
index 132cdce..a5c1284 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
@@ -25,6 +25,8 @@
import android.permission.PermissionManagerInternal;
import android.util.ArrayMap;
+import com.android.internal.util.function.QuadFunction;
+import com.android.internal.util.function.TriFunction;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
@@ -173,29 +175,47 @@
@PermissionInfo.ProtectionFlags int protectionFlags);
/**
- * Start delegate the permission identity of the shell UID to the given UID.
- *
- * @param uid the UID to delegate shell permission identity to
- * @param packageName the name of the package to delegate shell permission identity to
- * @param permissionNames the names of the permissions to delegate shell permission identity
- * for, or {@code null} for all permissions
+ * Sets the current check permission delegate
*/
- //@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
- void startShellPermissionIdentityDelegation(int uid,
- @NonNull String packageName, @Nullable List<String> permissionNames);
+ void setCheckPermissionDelegate(CheckPermissionDelegate delegate);
- /**
- * Stop delegating the permission identity of the shell UID.
- *
- * @see #startShellPermissionIdentityDelegation(int, String, List)
+ /**
+ * Interface to intercept permission checks and optionally pass through to the original
+ * implementation.
*/
- //@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
- void stopShellPermissionIdentityDelegation();
+ interface CheckPermissionDelegate {
- /**
- * Get all delegated shell permissions.
- */
- @NonNull List<String> getDelegatedShellPermissions();
+ /**
+ * Check whether the given package has been granted the specified permission.
+ *
+ * @param packageName the name of the package to be checked
+ * @param permissionName the name of the permission to be checked
+ * @param persistentDeviceId The persistent device ID
+ * @param userId the user ID
+ * @param superImpl the original implementation that can be delegated to
+ * @return {@link android.content.pm.PackageManager#PERMISSION_GRANTED} if the package has
+ * the permission, or {@link android.content.pm.PackageManager#PERMISSION_DENIED} otherwise
+ *
+ * @see android.content.pm.PackageManager#checkPermission(String, String)
+ */
+ int checkPermission(@NonNull String packageName, @NonNull String permissionName,
+ @NonNull String persistentDeviceId, @UserIdInt int userId,
+ @NonNull QuadFunction<String, String, String, Integer, Integer> superImpl);
+
+ /**
+ * Check whether the given UID has been granted the specified permission.
+ *
+ * @param uid the UID to be checked
+ * @param permissionName the name of the permission to be checked
+ * @param persistentDeviceId The persistent device ID
+ * @param superImpl the original implementation that can be delegated to
+ * @return {@link android.content.pm.PackageManager#PERMISSION_GRANTED} if the package has
+ * the permission, or {@link android.content.pm.PackageManager#PERMISSION_DENIED} otherwise
+ */
+ int checkUidPermission(int uid, @NonNull String permissionName,
+ @NonNull String persistentDeviceId,
+ @NonNull TriFunction<Integer, String, String, Integer> superImpl);
+ }
/**
* Read legacy permissions from legacy permission settings.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index fe280cb..0c689cb 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2705,7 +2705,11 @@
* Returns true if the specified UID has access to this display.
*/
boolean hasAccess(int uid) {
- return mDisplay.hasAccess(uid);
+ int userId = UserHandle.getUserId(uid);
+ boolean isUserVisibleOnDisplay = mWmService.mUmInternal.isUserVisible(
+ userId, mDisplayId);
+ return mDisplay.hasAccess(uid)
+ && (userId == UserHandle.USER_SYSTEM || isUserVisibleOnDisplay);
}
boolean isPrivate() {
@@ -4562,7 +4566,12 @@
}
}
- void attachAndShow(Transaction t) {
+ /**
+ * Attaches the snapshot of IME (a snapshot will be taken if there wasn't one) to the IME
+ * target task and shows it. If the given {@param anyTargetTask} is true, the snapshot won't
+ * be skipped by the activity type of IME target task.
+ */
+ void attachAndShow(Transaction t, boolean anyTargetTask) {
final DisplayContent dc = mImeTarget.getDisplayContent();
// Prepare IME screenshot for the target if it allows to attach into.
final Task task = mImeTarget.getTask();
@@ -4570,7 +4579,9 @@
final boolean renewImeSurface = mImeSurface == null
|| mImeSurface.getWidth() != dc.mInputMethodWindow.getFrame().width()
|| mImeSurface.getHeight() != dc.mInputMethodWindow.getFrame().height();
- if (task != null && !task.isActivityTypeHomeOrRecents()) {
+ // The exclusion of home/recents is an optimization for regular task switch because
+ // home/recents won't appear in recents task.
+ if (task != null && (anyTargetTask || !task.isActivityTypeHomeOrRecents())) {
ScreenCapture.ScreenshotHardwareBuffer imeBuffer = renewImeSurface
? dc.mWmService.mTaskSnapshotController.snapshotImeFromAttachedTask(task)
: null;
@@ -4638,7 +4649,9 @@
removeImeSurfaceImmediately();
mImeScreenshot = new ImeScreenshot(
mWmService.mSurfaceControlFactory.apply(null), imeTarget);
- mImeScreenshot.attachAndShow(t);
+ // If the caller requests to hide IME, then allow to show IME snapshot for any target task.
+ // So IME won't look like suddenly disappeared. It usually happens when turning off screen.
+ mImeScreenshot.attachAndShow(t, hideImeWindow /* anyTargetTask */);
if (mInputMethodWindow != null && hideImeWindow) {
// Hide the IME window when deciding to show IME snapshot on demand.
// InsetsController will make IME visible again before animating it.
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
index e015e97..9f3f297 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
@@ -161,7 +161,7 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+ @RequiresFlagsEnabled({Flags.FLAG_DE_HIDL, Flags.FLAG_FACE_VHAL_FEATURE})
public void registerAuthenticatorsLegacy_virtualOnly() throws Exception {
initService();
Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 8487021..3aebd70 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -101,6 +101,7 @@
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
+import android.view.WindowManagerGlobal;
import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.InputTransferToken;
@@ -556,6 +557,7 @@
.hasListener(eq(windowContextToken));
doReturn(TYPE_INPUT_METHOD).when(mWm.mWindowContextListenerController)
.getWindowType(eq(windowContextToken));
+ doReturn(true).when(mWm.mUmInternal).isUserVisible(anyInt(), anyInt());
mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, DEFAULT_DISPLAY,
UserHandle.USER_SYSTEM, WindowInsets.Type.defaultVisible(), null, new InsetsState(),
@@ -1308,6 +1310,24 @@
assertEquals(activityWindowInfo2, activityWindowInfo3);
}
+ @Test
+ public void testAddOverlayWindowToUnassignedDisplay_notAllowed() {
+ int uid = 100000; // uid for non-system user
+ Session session = createTestSession(mAtm, 1234 /* pid */, uid);
+ DisplayContent dc = createNewDisplay();
+ int displayId = dc.getDisplayId();
+ int userId = UserHandle.getUserId(uid);
+ doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ LayoutParams.TYPE_APPLICATION_OVERLAY);
+
+ int result = mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, displayId,
+ userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(),
+ new InsetsSourceControl.Array(), new Rect(), new float[1]);
+
+ assertThat(result).isEqualTo(WindowManagerGlobal.ADD_INVALID_DISPLAY);
+ }
+
class TestResultReceiver implements IResultReceiver {
public android.os.Bundle resultData;
private final IBinder mBinder = mock(IBinder.class);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index ebdd556..10c17c1 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9890,6 +9890,16 @@
"satellite_entitlement_app_name_string";
/**
+ * URL to redirect user to get more information about the carrier support for satellite.
+ *
+ * The default value is empty string.
+ *
+ * @hide
+ */
+ public static final String KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING =
+ "satellite_information_redirect_url_string";
+
+ /**
* Indicating whether DUN APN should be disabled when the device is roaming. In that case,
* the default APN (i.e. internet) will be used for tethering.
*
@@ -11034,6 +11044,7 @@
sDefaults.putInt(KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT, 7);
sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false);
sDefaults.putString(KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING, "androidSatmode");
+ sDefaults.putString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING, "");
sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);