diff options
300 files changed, 8012 insertions, 3500 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 071cc6555be7..e5c059ecbfb7 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -84,7 +84,7 @@ aconfig_declarations_group { "android.view.inputmethod.flags-aconfig-java", "android.webkit.flags-aconfig-java", "android.widget.flags-aconfig-java", - "android.xr.flags-aconfig-java", + "android.xr.flags-aconfig-java-export", "art_exported_aconfig_flags_lib", "backstage_power_flags_lib", "backup_flags_lib", @@ -989,15 +989,22 @@ java_aconfig_library { // XR aconfig_declarations { name: "android.xr.flags-aconfig", - package: "android.xr", container: "system", + exportable: true, + package: "android.xr", srcs: ["core/java/android/content/pm/xr.aconfig"], } java_aconfig_library { - name: "android.xr.flags-aconfig-java", + name: "android.xr.flags-aconfig-java-export", aconfig_declarations: "android.xr.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], + min_sdk_version: "30", + mode: "exported", + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], } // android.app diff --git a/Android.bp b/Android.bp index 9d3b64d7335b..303fa2cd18da 100644 --- a/Android.bp +++ b/Android.bp @@ -583,6 +583,7 @@ java_library { "documents-ui-compat-config", "calendar-provider-compat-config", "contacts-provider-platform-compat-config", + "SystemUI-core-compat-config", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { "true": [], default: [ diff --git a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java index 8e3ed6d9931c..7a7250b9e910 100644 --- a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java +++ b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java @@ -19,27 +19,24 @@ package android.view; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import android.content.Context; -import android.perftests.utils.BenchmarkState; -import android.perftests.utils.PerfStatusReporter; -import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.benchmark.BenchmarkState; +import androidx.benchmark.junit4.BenchmarkRule; +import androidx.test.filters.SmallTest; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -@LargeTest -@RunWith(AndroidJUnit4.class) +@SmallTest public class ViewConfigurationPerfTest { @Rule - public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + public final BenchmarkRule mBenchmarkRule = new BenchmarkRule(); private final Context mContext = getInstrumentation().getTargetContext(); @Test public void testGet_newViewConfiguration() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final BenchmarkState state = mBenchmarkRule.getState(); while (state.keepRunning()) { state.pauseTiming(); @@ -53,7 +50,7 @@ public class ViewConfigurationPerfTest { @Test public void testGet_cachedViewConfiguration() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final BenchmarkState state = mBenchmarkRule.getState(); // Do `get` once to make sure there's something cached. ViewConfiguration.get(mContext); @@ -61,265 +58,4 @@ public class ViewConfigurationPerfTest { ViewConfiguration.get(mContext); } } - - @Test - public void testGetPressedStateDuration_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getPressedStateDuration(); - } - } - - @Test - public void testGetPressedStateDuration_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getPressedStateDuration(); - - while (state.keepRunning()) { - ViewConfiguration.getPressedStateDuration(); - } - } - - @Test - public void testGetTapTimeout_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getTapTimeout(); - } - } - - @Test - public void testGetTapTimeout_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getTapTimeout(); - - while (state.keepRunning()) { - ViewConfiguration.getTapTimeout(); - } - } - - @Test - public void testGetJumpTapTimeout_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getJumpTapTimeout(); - } - } - - @Test - public void testGetJumpTapTimeout_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getJumpTapTimeout(); - - while (state.keepRunning()) { - ViewConfiguration.getJumpTapTimeout(); - } - } - - @Test - public void testGetDoubleTapTimeout_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getDoubleTapTimeout(); - } - } - - @Test - public void testGetDoubleTapTimeout_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getDoubleTapTimeout(); - - while (state.keepRunning()) { - ViewConfiguration.getDoubleTapTimeout(); - } - } - - @Test - public void testGetDoubleTapMinTime_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getDoubleTapMinTime(); - } - } - - @Test - public void testGetDoubleTapMinTime_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getDoubleTapMinTime(); - - while (state.keepRunning()) { - ViewConfiguration.getDoubleTapMinTime(); - } - } - - @Test - public void testGetZoomControlsTimeout_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getZoomControlsTimeout(); - } - } - - @Test - public void testGetZoomControlsTimeout_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getZoomControlsTimeout(); - - while (state.keepRunning()) { - ViewConfiguration.getZoomControlsTimeout(); - } - } - - @Test - public void testGetLongPressTimeout() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - ViewConfiguration.getLongPressTimeout(); - } - } - - @Test - public void testGetMultiPressTimeout() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - ViewConfiguration.getMultiPressTimeout(); - } - } - - @Test - public void testGetKeyRepeatTimeout() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - ViewConfiguration.getKeyRepeatTimeout(); - } - } - - @Test - public void testGetKeyRepeatDelay() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - ViewConfiguration.getKeyRepeatDelay(); - } - } - - @Test - public void testGetHoverTapSlop_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getHoverTapSlop(); - } - } - - @Test - public void testGetHoverTapSlop_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getHoverTapSlop(); - - while (state.keepRunning()) { - ViewConfiguration.getHoverTapSlop(); - } - } - - @Test - public void testGetScrollFriction_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getScrollFriction(); - } - } - - @Test - public void testGetScrollFriction_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getScrollFriction(); - - while (state.keepRunning()) { - ViewConfiguration.getScrollFriction(); - } - } - - @Test - public void testGetDefaultActionModeHideDuration_unCached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - - while (state.keepRunning()) { - state.pauseTiming(); - // Reset any caches. - ViewConfiguration.resetCacheForTesting(); - state.resumeTiming(); - - ViewConfiguration.getDefaultActionModeHideDuration(); - } - } - - @Test - public void testGetDefaultActionModeHideDuration_cached() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - // Do `get` once to make sure the value gets cached. - ViewConfiguration.getDefaultActionModeHideDuration(); - - while (state.keepRunning()) { - ViewConfiguration.getDefaultActionModeHideDuration(); - } - } } diff --git a/api/OWNERS b/api/OWNERS index 965093c9ab38..f2bcf13d2d2e 100644 --- a/api/OWNERS +++ b/api/OWNERS @@ -9,4 +9,4 @@ per-file *.go,go.mod,go.work,go.work.sum = file:platform/build/soong:/OWNERS per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION} # For metalava team to disable lint checks in platform -per-file Android.bp = aurimas@google.com,emberrose@google.com +per-file Android.bp = aurimas@google.com diff --git a/cmds/am/am.sh b/cmds/am/am.sh index 76ec214cb446..f099be3e26a2 100755 --- a/cmds/am/am.sh +++ b/cmds/am/am.sh @@ -1,11 +1,10 @@ #!/system/bin/sh -# set to top-app process group -settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true - if [ "$1" != "instrument" ] ; then cmd activity "$@" else + # set to top-app process group for instrument + settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true base=/system export CLASSPATH=$base/framework/am.jar exec app_process $base/bin com.android.commands.am.Am "$@" diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java index 6310d32515c5..696bc82a9ffc 100644 --- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java +++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java @@ -18,6 +18,7 @@ package com.android.commands.bmgr; import android.annotation.IntDef; import android.annotation.UserIdInt; +import android.app.ActivityManager; import android.app.backup.BackupManager; import android.app.backup.BackupManagerMonitor; import android.app.backup.BackupProgress; @@ -73,6 +74,8 @@ public class Bmgr { "Error: Could not access the backup transport. Is the system running?"; private static final String PM_NOT_RUNNING_ERR = "Error: Could not access the Package Manager. Is the system running?"; + private static final String INVALID_USER_ID_ERR_TEMPLATE = + "Error: Invalid user id (%d).\n"; private String[] mArgs; private int mNextArg; @@ -104,6 +107,11 @@ public class Bmgr { mArgs = args; mNextArg = 0; int userId = parseUserId(); + if (userId < 0) { + System.err.printf(INVALID_USER_ID_ERR_TEMPLATE, userId); + return; + } + String op = nextArg(); Slog.v(TAG, "Running " + op + " for user:" + userId); @@ -955,12 +963,15 @@ public class Bmgr { private int parseUserId() { String arg = nextArg(); - if ("--user".equals(arg)) { - return UserHandle.parseUserArg(nextArg()); - } else { + if (!"--user".equals(arg)) { mNextArg--; return UserHandle.USER_SYSTEM; } + int userId = UserHandle.parseUserArg(nextArg()); + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + return userId; } private static void showUsage() { diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index b43905b19239..844e52c3ecf2 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -441,7 +441,7 @@ public: numEvents = mBootAnimation->mDisplayEventReceiver->getEvents(buffer, kBufferSize); for (size_t i = 0; i < static_cast<size_t>(numEvents); i++) { const auto& event = buffer[i]; - if (event.header.type == DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG) { + if (event.header.type == DisplayEventType::DISPLAY_EVENT_HOTPLUG) { SLOGV("Hotplug received"); if (!event.hotplug.connected) { diff --git a/cmds/uinput/tests/Android.bp b/cmds/uinput/tests/Android.bp index e728bd270a46..516de3325f77 100644 --- a/cmds/uinput/tests/Android.bp +++ b/cmds/uinput/tests/Android.bp @@ -18,3 +18,17 @@ android_test { "device-tests", ], } + +android_ravenwood_test { + name: "UinputTestsRavenwood", + srcs: [ + "src/**/*.java", + ], + static_libs: [ + "androidx.test.runner", + "frameworks-base-testutils", + "platform-test-annotations", + "truth", + "uinput", + ], +} diff --git a/core/java/android/accessibilityservice/OWNERS b/core/java/android/accessibilityservice/OWNERS index 1265dfa2c441..dac64f47ba7e 100644 --- a/core/java/android/accessibilityservice/OWNERS +++ b/core/java/android/accessibilityservice/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS
\ No newline at end of file diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 82c746a8ad4c..b8c20bd97264 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -2230,6 +2230,16 @@ public class ActivityOptions extends ComponentOptions { return mLaunchCookie; } + /** + * Set the ability for the current transition/animation to work cross-task. + * @param allowTaskOverride true to allow cross-task use, otherwise false. + * + * @hide + */ + public ActivityOptions setOverrideTaskTransition(boolean allowTaskOverride) { + this.mOverrideTaskTransition = allowTaskOverride; + return this; + } /** @hide */ public boolean getOverrideTaskTransition() { diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index c3dc257e6535..fcdb02ab5da2 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -125,11 +125,3 @@ flag { description: "Show virtual devices in Settings" bug: "338974320" } - -flag { - name: "migrate_viewconfiguration_constants_to_resources" - namespace: "virtual_devices" - description: "Use resources instead of constants in ViewConfiguration" - is_fixed_read_only: true - bug: "370928384" -} diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index fded88212127..d8919160320a 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -641,6 +641,9 @@ public final class DisplayManager { * is triggered whenever the properties of a {@link android.view.Display}, such as size, * state, density are modified. * + * This event is not triggered for refresh rate changes as they can change very often. + * To monitor refresh rate changes, subscribe to {@link EVENT_TYPE_DISPLAY_REFRESH_RATE}. + * * @see #registerDisplayListener(DisplayListener, Handler, long) * */ @@ -839,6 +842,9 @@ public final class DisplayManager { * Registers a display listener to receive notifications about when * displays are added, removed or changed. * + * We encourage to use {@link #registerDisplayListener(Executor, long, DisplayListener)} + * instead to subscribe for explicit events of interest + * * @param listener The listener to register. * @param handler The handler on which the listener should be invoked, or null * if the listener should be invoked on the calling thread's looper. @@ -847,7 +853,9 @@ public final class DisplayManager { */ public void registerDisplayListener(DisplayListener listener, Handler handler) { registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED - | EVENT_TYPE_DISPLAY_CHANGED | EVENT_TYPE_DISPLAY_REMOVED); + | EVENT_TYPE_DISPLAY_CHANGED + | EVENT_TYPE_DISPLAY_REFRESH_RATE + | EVENT_TYPE_DISPLAY_REMOVED); } /** diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index b5715ed25bd9..339dbf2c2029 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -1766,29 +1766,23 @@ public final class DisplayManagerGlobal { } if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_CHANGED) != 0) { - // For backward compatibility, a client subscribing to - // DisplayManager.EVENT_FLAG_DISPLAY_CHANGED will be enrolled to both Basic and - // RR changes - baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED - | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED; } - if ((eventFlags - & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) { + if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; } - if (Flags.displayListenerPerformanceImprovements()) { - if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) { - baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; - } + if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) { + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + } + if (Flags.displayListenerPerformanceImprovements()) { if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_STATE) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_STATE; } } - return baseEventMask; } } diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 23722ed5bb0d..8d58296e5581 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -233,3 +233,12 @@ flag { description: "Key Event Activity Detection" bug: "356412905" } + +flag { + name: "enable_backup_and_restore_for_input_gestures" + namespace: "input" + description: "Adds backup and restore support for custom input gestures" + bug: "382184249" + is_fixed_read_only: true +} + diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 91ad22f51345..24f8672c1e7c 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -22,6 +22,7 @@ import static android.app.AppOpsManager.OP_LEGACY_STORAGE; import static android.app.AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE; import static android.app.AppOpsManager.OP_READ_EXTERNAL_STORAGE; import static android.app.AppOpsManager.OP_READ_MEDIA_IMAGES; +import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM; import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.UserHandle.PER_USER_RANGE; @@ -44,6 +45,7 @@ import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.PendingIntent; +import android.app.PropertyInvalidatedCache; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; import android.content.Context; @@ -269,14 +271,15 @@ public class StorageManager { public static final int FLAG_STORAGE_SDK = IInstalld.FLAG_STORAGE_SDK; /** {@hide} */ - @IntDef(prefix = "FLAG_STORAGE_", value = { + @IntDef(prefix = "FLAG_STORAGE_", value = { FLAG_STORAGE_DE, FLAG_STORAGE_CE, FLAG_STORAGE_EXTERNAL, FLAG_STORAGE_SDK, }) @Retention(RetentionPolicy.SOURCE) - public @interface StorageFlags {} + public @interface StorageFlags { + } /** {@hide} */ public static final int FLAG_FOR_WRITE = 1 << 8; @@ -309,6 +312,44 @@ public class StorageManager { @GuardedBy("mDelegates") private final ArrayList<StorageEventListenerDelegate> mDelegates = new ArrayList<>(); + static record VolumeListQuery(int mUserId, String mPackageName, int mFlags) { + } + + private static final PropertyInvalidatedCache.QueryHandler<VolumeListQuery, StorageVolume[]> + sVolumeListQuery = new PropertyInvalidatedCache.QueryHandler<>() { + @androidx.annotation.Nullable + @Override + public StorageVolume[] apply(@androidx.annotation.NonNull VolumeListQuery query) { + final IStorageManager storageManager = IStorageManager.Stub.asInterface( + ServiceManager.getService("mount")); + if (storageManager == null) { + // negative results won't be cached, so we will just try again next time + return null; + } + try { + return storageManager.getVolumeList( + query.mUserId, query.mPackageName, query.mFlags); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + }; + + // Generally, the userId and packageName parameters stay pretty constant, but flags may change + // regularly; we have observed some processes hitting 10+ variations. + private static final int VOLUME_LIST_CACHE_MAX = 16; + + private static final PropertyInvalidatedCache<VolumeListQuery, StorageVolume[]> + sVolumeListCache = new PropertyInvalidatedCache<>( + new PropertyInvalidatedCache.Args(MODULE_SYSTEM).cacheNulls(false) + .api("getVolumeList").maxEntries(VOLUME_LIST_CACHE_MAX), "getVolumeList", + sVolumeListQuery); + + /** {@hide} */ + public static void invalidateVolumeListCache() { + sVolumeListCache.invalidateCache(); + } + private class StorageEventListenerDelegate extends IStorageEventListener.Stub { final Executor mExecutor; final StorageEventListener mListener; @@ -395,7 +436,8 @@ public class StorageManager { private class ObbActionListener extends IObbActionListener.Stub { @SuppressWarnings("hiding") - private SparseArray<ObbListenerDelegate> mListeners = new SparseArray<ObbListenerDelegate>(); + private SparseArray<ObbListenerDelegate> mListeners = + new SparseArray<ObbListenerDelegate>(); @Override public void onObbResult(String filename, int nonce, int status) { @@ -477,10 +519,10 @@ public class StorageManager { * * @param looper The {@link android.os.Looper} which events will be received on. * - * <p>Applications can get instance of this class by calling - * {@link android.content.Context#getSystemService(java.lang.String)} with an argument - * of {@link android.content.Context#STORAGE_SERVICE}. - * + * <p>Applications can get instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String)} with an + * argument + * of {@link android.content.Context#STORAGE_SERVICE}. * @hide */ @UnsupportedAppUsage @@ -488,15 +530,16 @@ public class StorageManager { mContext = context; mResolver = context.getContentResolver(); mLooper = looper; - mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getServiceOrThrow("mount")); + mStorageManager = IStorageManager.Stub.asInterface( + ServiceManager.getServiceOrThrow("mount")); mAppOps = mContext.getSystemService(AppOpsManager.class); } /** * Registers a {@link android.os.storage.StorageEventListener StorageEventListener}. * - * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. - * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} + * object. * @hide */ @UnsupportedAppUsage @@ -516,14 +559,14 @@ public class StorageManager { /** * Unregisters a {@link android.os.storage.StorageEventListener StorageEventListener}. * - * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. - * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} + * object. * @hide */ @UnsupportedAppUsage public void unregisterListener(StorageEventListener listener) { synchronized (mDelegates) { - for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) { + for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) { final StorageEventListenerDelegate delegate = i.next(); if (delegate.mListener == listener) { try { @@ -558,7 +601,8 @@ public class StorageManager { * {@link StorageManager#getStorageVolumes()} to observe the latest * value. */ - public void onStateChanged(@NonNull StorageVolume volume) { } + public void onStateChanged(@NonNull StorageVolume volume) { + } } /** @@ -592,7 +636,7 @@ public class StorageManager { */ public void unregisterStorageVolumeCallback(@NonNull StorageVolumeCallback callback) { synchronized (mDelegates) { - for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) { + for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) { final StorageEventListenerDelegate delegate = i.next(); if (delegate.mCallback == callback) { try { @@ -628,8 +672,8 @@ public class StorageManager { /** * Query if a USB Mass Storage (UMS) host is connected. - * @return true if UMS host is connected. * + * @return true if UMS host is connected. * @hide */ @Deprecated @@ -640,8 +684,8 @@ public class StorageManager { /** * Query if a USB Mass Storage (UMS) is enabled on the device. - * @return true if UMS host is enabled. * + * @return true if UMS host is enabled. * @hide */ @Deprecated @@ -663,11 +707,11 @@ public class StorageManager { * That is, shared UID applications can attempt to mount any other * application's OBB that shares its UID. * - * @param rawPath the path to the OBB file - * @param key must be <code>null</code>. Previously, some Android device - * implementations accepted a non-<code>null</code> key to mount - * an encrypted OBB file. However, this never worked reliably and - * is no longer supported. + * @param rawPath the path to the OBB file + * @param key must be <code>null</code>. Previously, some Android device + * implementations accepted a non-<code>null</code> key to mount + * an encrypted OBB file. However, this never worked reliably and + * is no longer supported. * @param listener will receive the success or failure of the operation * @return whether the mount call was successfully queued or not */ @@ -739,9 +783,9 @@ public class StorageManager { * application's OBB that shares its UID. * <p> * - * @param rawPath path to the OBB file - * @param force whether to kill any programs using this in order to unmount - * it + * @param rawPath path to the OBB file + * @param force whether to kill any programs using this in order to unmount + * it * @param listener will receive the success or failure of the operation * @return whether the unmount call was successfully queued or not */ @@ -781,7 +825,7 @@ public class StorageManager { * * @param rawPath path to OBB image * @return absolute path to mounted OBB image data or <code>null</code> if - * not mounted or exception encountered trying to read status + * not mounted or exception encountered trying to read status */ public String getMountedObbPath(String rawPath) { Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); @@ -899,7 +943,7 @@ public class StorageManager { * {@link #UUID_DEFAULT}. * * @throws IOException when the storage device hosting the given path isn't - * present, or when it doesn't have a valid UUID. + * present, or when it doesn't have a valid UUID. */ public @NonNull UUID getUuidForPath(@NonNull File path) throws IOException { Preconditions.checkNotNull(path); @@ -1172,8 +1216,8 @@ public class StorageManager { /** * This is not the API you're looking for. * - * @see PackageManager#getPrimaryStorageCurrentVolume() * @hide + * @see PackageManager#getPrimaryStorageCurrentVolume() */ public String getPrimaryStorageUuid() { try { @@ -1186,8 +1230,8 @@ public class StorageManager { /** * This is not the API you're looking for. * - * @see PackageManager#movePrimaryStorage(VolumeInfo) * @hide + * @see PackageManager#movePrimaryStorage(VolumeInfo) */ public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) { try { @@ -1216,7 +1260,7 @@ public class StorageManager { // resolve the actual volume name if (Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL)) { try (Cursor c = mContext.getContentResolver().query(uri, - new String[] { MediaStore.MediaColumns.VOLUME_NAME }, null, null)) { + new String[]{MediaStore.MediaColumns.VOLUME_NAME}, null, null)) { if (c.moveToFirst()) { volumeName = c.getString(0); } @@ -1275,6 +1319,7 @@ public class StorageManager { /** * Gets the state of a volume via its mountpoint. + * * @hide */ @Deprecated @@ -1308,7 +1353,7 @@ public class StorageManager { * Return the list of shared/external storage volumes currently available to * the calling user and the user it shares media with. Please refer to * <a href="https://source.android.com/compatibility/12/android-12-cdd#95_multi-user_support"> - * multi-user support</a> for more details. + * multi-user support</a> for more details. * * <p> * This is similar to {@link StorageManager#getStorageVolumes()} except that the result also @@ -1353,7 +1398,7 @@ public class StorageManager { public static Pair<String, Long> getPrimaryStoragePathAndSize() { return Pair.create(null, FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace() - + Environment.getRootDirectory().getTotalSpace())); + + Environment.getRootDirectory().getTotalSpace())); } /** {@hide} */ @@ -1389,8 +1434,6 @@ public class StorageManager { /** {@hide} */ @UnsupportedAppUsage public static @NonNull StorageVolume[] getVolumeList(int userId, int flags) { - final IStorageManager storageManager = IStorageManager.Stub.asInterface( - ServiceManager.getService("mount")); try { String packageName = ActivityThread.currentOpPackageName(); if (packageName == null) { @@ -1406,7 +1449,7 @@ public class StorageManager { } packageName = packageNames[0]; } - return storageManager.getVolumeList(userId, packageName, flags); + return sVolumeListCache.query(new VolumeListQuery(userId, packageName, flags)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1414,6 +1457,7 @@ public class StorageManager { /** * Returns list of paths for all mountable volumes. + * * @hide */ @Deprecated @@ -1605,7 +1649,7 @@ public class StorageManager { * <p> * This is only intended to be called by UserManagerService, as part of creating a user. * - * @param userId ID of the user + * @param userId ID of the user * @param ephemeral whether the user is ephemeral * @throws RuntimeException on error. The user's keys already existing is considered an error. * @hide @@ -1711,7 +1755,8 @@ public class StorageManager { return false; } - /** {@hide} + /** + * {@hide} * Is this device encrypted? * <p> * Note: all devices launching with Android 10 (API level 29) or later are @@ -1724,8 +1769,10 @@ public class StorageManager { return RoSystemProperties.CRYPTO_ENCRYPTED; } - /** {@hide} + /** + * {@hide} * Does this device have file-based encryption (FBE) enabled? + * * @return true if the device has file-based encryption enabled. */ public static boolean isFileEncrypted() { @@ -1759,8 +1806,8 @@ public class StorageManager { } /** - * @deprecated disabled now that FUSE has been replaced by sdcardfs * @hide + * @deprecated disabled now that FUSE has been replaced by sdcardfs */ @Deprecated public static File maybeTranslateEmulatedPathToInternal(File path) { @@ -1790,6 +1837,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop. + * * @hide */ public static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid, @@ -1800,6 +1848,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop but do not noteOp. + * * @hide */ public static boolean checkPermissionAndCheckOp(Context context, boolean enforce, @@ -1810,6 +1859,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop. + * * @hide */ private static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid, @@ -1877,7 +1927,9 @@ public class StorageManager { // Legacy apps technically have the access granted by this op, // even when the op is denied if ((mAppOps.checkOpNoThrow(OP_LEGACY_STORAGE, uid, - packageName) == AppOpsManager.MODE_ALLOWED)) return true; + packageName) == AppOpsManager.MODE_ALLOWED)) { + return true; + } if (enforce) { throw new SecurityException("Op " + AppOpsManager.opToName(op) + " " @@ -1924,7 +1976,7 @@ public class StorageManager { return true; } if (mode == AppOpsManager.MODE_DEFAULT && mContext.checkPermission( - MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) { + MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) { return true; } // If app doesn't have MANAGE_EXTERNAL_STORAGE, then check if it has requested granular @@ -1936,7 +1988,7 @@ public class StorageManager { @VisibleForTesting public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback, Handler handler, ThreadFactory factory) - throws IOException { + throws IOException { Preconditions.checkNotNull(callback); MetricsLogger.count(mContext, "storage_open_proxy_file_descriptor", 1); // Retry is needed because the mount point mFuseAppLoop is using may be unmounted before @@ -1987,7 +2039,7 @@ public class StorageManager { /** {@hide} */ public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback) - throws IOException { + throws IOException { return openProxyFileDescriptor(mode, callback, null, null); } @@ -2006,19 +2058,18 @@ public class StorageManager { * you're willing to decrypt on-demand, but where you want to avoid * persisting the cleartext version. * - * @param mode The desired access mode, must be one of - * {@link ParcelFileDescriptor#MODE_READ_ONLY}, - * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or - * {@link ParcelFileDescriptor#MODE_READ_WRITE} + * @param mode The desired access mode, must be one of + * {@link ParcelFileDescriptor#MODE_READ_ONLY}, + * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or + * {@link ParcelFileDescriptor#MODE_READ_WRITE} * @param callback Callback to process file operation requests issued on - * returned file descriptor. - * @param handler Handler that invokes callback methods. + * returned file descriptor. + * @param handler Handler that invokes callback methods. * @return Seekable ParcelFileDescriptor. - * @throws IOException */ public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback, Handler handler) - throws IOException { + throws IOException { Preconditions.checkNotNull(handler); return openProxyFileDescriptor(mode, callback, handler, null); } @@ -2050,10 +2101,10 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume that you're interested - * in. The UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * in. The UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support cache quotas. + * doesn't support cache quotas. * @see #getCacheSizeBytes(UUID) */ @WorkerThread @@ -2085,10 +2136,10 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume that you're interested - * in. The UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * in. The UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support cache quotas. + * doesn't support cache quotas. * @see #getCacheQuotaBytes(UUID) */ @WorkerThread @@ -2106,7 +2157,7 @@ public class StorageManager { /** @hide */ - @IntDef(prefix = { "MOUNT_MODE_" }, value = { + @IntDef(prefix = {"MOUNT_MODE_"}, value = { MOUNT_MODE_EXTERNAL_NONE, MOUNT_MODE_EXTERNAL_DEFAULT, MOUNT_MODE_EXTERNAL_INSTALLER, @@ -2115,16 +2166,19 @@ public class StorageManager { }) @Retention(RetentionPolicy.SOURCE) /** @hide */ - public @interface MountMode {} + public @interface MountMode { + } /** * No external storage should be mounted. + * * @hide */ @SystemApi public static final int MOUNT_MODE_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE; /** * Default external storage should be mounted. + * * @hide */ @SystemApi @@ -2132,12 +2186,14 @@ public class StorageManager { /** * Mount mode for package installers which should give them access to * all obb dirs in addition to their package sandboxes + * * @hide */ @SystemApi public static final int MOUNT_MODE_EXTERNAL_INSTALLER = IVold.REMOUNT_MODE_INSTALLER; /** * The lower file system should be bind mounted directly on external storage + * * @hide */ @SystemApi @@ -2146,6 +2202,7 @@ public class StorageManager { /** * Use the regular scoped storage filesystem, but Android/ should be writable. * Used to support the applications hosting DownloadManager and the MTP server. + * * @hide */ @SystemApi @@ -2164,10 +2221,10 @@ public class StorageManager { * this flag to take effect. * </p> * + * @hide * @see #getAllocatableBytes(UUID, int) * @see #allocateBytes(UUID, long, int) * @see #allocateBytes(FileDescriptor, long, int) - * @hide */ @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) @SystemApi @@ -2194,6 +2251,7 @@ public class StorageManager { * freeable cached space when determining allocatable space. * * Intended for use with {@link #getAllocatableBytes()}. + * * @hide */ public static final int FLAG_ALLOCATE_NON_CACHE_ONLY = 1 << 3; @@ -2203,12 +2261,13 @@ public class StorageManager { * cached space when determining allocatable space. * * Intended for use with {@link #getAllocatableBytes()}. + * * @hide */ public static final int FLAG_ALLOCATE_CACHE_ONLY = 1 << 4; /** @hide */ - @IntDef(flag = true, prefix = { "FLAG_ALLOCATE_" }, value = { + @IntDef(flag = true, prefix = {"FLAG_ALLOCATE_"}, value = { FLAG_ALLOCATE_AGGRESSIVE, FLAG_ALLOCATE_DEFY_ALL_RESERVED, FLAG_ALLOCATE_DEFY_HALF_RESERVED, @@ -2216,7 +2275,8 @@ public class StorageManager { FLAG_ALLOCATE_CACHE_ONLY, }) @Retention(RetentionPolicy.SOURCE) - public @interface AllocateFlags {} + public @interface AllocateFlags { + } /** * Return the maximum number of new bytes that your app can allocate for @@ -2246,15 +2306,15 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume where you're - * considering allocating disk space, since allocatable space can - * vary widely depending on the underlying storage device. The - * UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * considering allocating disk space, since allocatable space can + * vary widely depending on the underlying storage device. The + * UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @return the maximum number of new bytes that the calling app can allocate - * using {@link #allocateBytes(UUID, long)} or - * {@link #allocateBytes(FileDescriptor, long)}. + * using {@link #allocateBytes(UUID, long)} or + * {@link #allocateBytes(FileDescriptor, long)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space. + * doesn't support allocating space. */ @WorkerThread public @BytesLong long getAllocatableBytes(@NonNull UUID storageUuid) @@ -2297,12 +2357,12 @@ public class StorageManager { * more than once every 60 seconds. * * @param storageUuid the UUID of the storage volume where you'd like to - * allocate disk space. The UUID for a specific path can be - * obtained using {@link #getUuidForPath(File)}. - * @param bytes the number of bytes to allocate. + * allocate disk space. The UUID for a specific path can be + * obtained using {@link #getUuidForPath(File)}. + * @param bytes the number of bytes to allocate. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space, or if the device had - * trouble allocating the requested space. + * doesn't support allocating space, or if the device had + * trouble allocating the requested space. * @see #getAllocatableBytes(UUID) */ @WorkerThread @@ -2332,10 +2392,9 @@ public class StorageManager { * These mount modes specify different views and access levels for * different apps on external storage. * + * @return {@code MountMode} for the given uid and packageName. * @params uid UID of the application * @params packageName name of the package - * @return {@code MountMode} for the given uid and packageName. - * * @hide */ @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) @@ -2366,15 +2425,15 @@ public class StorageManager { * (such as when recording a video) you should avoid calling this method * more than once every 60 seconds. * - * @param fd the open file that you'd like to allocate disk space for. + * @param fd the open file that you'd like to allocate disk space for. * @param bytes the number of bytes to allocate. This is the desired final - * size of the open file. If the open file is smaller than this - * requested size, it will be extended without modifying any - * existing contents. If the open file is larger than this - * requested size, it will be truncated. + * size of the open file. If the open file is smaller than this + * requested size, it will be extended without modifying any + * existing contents. If the open file is larger than this + * requested size, it will be truncated. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space, or if the device had - * trouble allocating the requested space. + * doesn't support allocating space, or if the device had + * trouble allocating the requested space. * @see #isAllocationSupported(FileDescriptor) * @see Environment#isExternalStorageEmulated(File) */ @@ -2499,13 +2558,14 @@ public class StorageManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "QUOTA_TYPE_" }, value = { + @IntDef(prefix = {"QUOTA_TYPE_"}, value = { QUOTA_TYPE_MEDIA_NONE, QUOTA_TYPE_MEDIA_AUDIO, QUOTA_TYPE_MEDIA_VIDEO, QUOTA_TYPE_MEDIA_IMAGE, }) - public @interface QuotaType {} + public @interface QuotaType { + } private static native boolean setQuotaProjectId(String path, long projectId); @@ -2532,15 +2592,13 @@ public class StorageManager { * The default platform user of this API is the MediaProvider process, which is * responsible for managing all of external storage. * - * @param path the path to the file for which we should update the quota type + * @param path the path to the file for which we should update the quota type * @param quotaType the quota type of the file; this is based on the * {@code QuotaType} constants, eg * {@code StorageManager.QUOTA_TYPE_MEDIA_AUDIO} - * * @throws IllegalArgumentException if {@code quotaType} does not correspond to a valid * quota type. * @throws IOException if the quota type could not be updated. - * * @hide */ @SystemApi @@ -2616,7 +2674,6 @@ public class StorageManager { * permissions of a directory to what they should anyway be. * * @param path the path for which we should fix up the permissions - * * @hide */ public void fixupAppDir(@NonNull File path) { @@ -2822,11 +2879,12 @@ public class StorageManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "APP_IO_BLOCKED_REASON_" }, value = { - APP_IO_BLOCKED_REASON_TRANSCODING, - APP_IO_BLOCKED_REASON_UNKNOWN, + @IntDef(prefix = {"APP_IO_BLOCKED_REASON_"}, value = { + APP_IO_BLOCKED_REASON_TRANSCODING, + APP_IO_BLOCKED_REASON_UNKNOWN, }) - public @interface AppIoBlockedReason {} + public @interface AppIoBlockedReason { + } /** * Notify the system that an app with {@code uid} and {@code tid} is blocked on an IO request on @@ -2839,10 +2897,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume that the app IO is blocked on - * @param uid the UID of the app blocked on IO - * @param tid the tid of the app blocked on IO - * @param reason the reason the app is blocked on IO - * + * @param uid the UID of the app blocked on IO + * @param tid the tid of the app blocked on IO + * @param reason the reason the app is blocked on IO * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2866,10 +2923,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume that the app IO is resumed on - * @param uid the UID of the app resuming IO - * @param tid the tid of the app resuming IO - * @param reason the reason the app is resuming IO - * + * @param uid the UID of the app resuming IO + * @param tid the tid of the app resuming IO + * @param reason the reason the app is resuming IO * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2890,10 +2946,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume to check IO blocked status - * @param uid the UID of the app to check IO blocked status - * @param tid the tid of the app to check IO blocked status - * @param reason the reason to check IO blocked status for - * + * @param uid the UID of the app to check IO blocked status + * @param tid the tid of the app to check IO blocked status + * @param reason the reason to check IO blocked status for * @hide */ @TestApi @@ -2962,7 +3017,6 @@ public class StorageManager { * information is available, -1 is returned. * * @return Percentage of the remaining useful lifetime of the internal storage device. - * * @hide */ @FlaggedApi(Flags.FLAG_STORAGE_LIFETIME_API) diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index 195896dc8edf..0e78bfdb5069 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -180,7 +180,7 @@ public final class InputEventConsistencyVerifier { final MotionEvent motionEvent = (MotionEvent)event; if (motionEvent.isTouchEvent()) { onTouchEvent(motionEvent, nestingLevel); - } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + } else if (motionEvent.isFromSource(InputDevice.SOURCE_TRACKBALL)) { onTrackballEvent(motionEvent, nestingLevel); } else { onGenericMotionEvent(motionEvent, nestingLevel); diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 1c36eaf99afa..9c1f134bff3e 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -290,9 +290,15 @@ public abstract class InputEventReceiver { @SuppressWarnings("unused") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void dispatchInputEvent(int seq, InputEvent event) { - Trace.traceBegin(Trace.TRACE_TAG_INPUT, "dispatchInputEvent " + getShortDescription(event)); + if (Trace.isTagEnabled(Trace.TRACE_TAG_INPUT)) { + // This 'if' block is an optimization - without it, 'getShortDescription' will be + // called unconditionally, which is expensive. + Trace.traceBegin(Trace.TRACE_TAG_INPUT, + "dispatchInputEvent " + getShortDescription(event)); + } mSeqMap.put(event.getSequenceNumber(), seq); onInputEvent(event); + // If tracing is not enabled, `traceEnd` is a no-op (so we don't need to guard it with 'if') Trace.traceEnd(Trace.TRACE_TAG_INPUT); } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 2895bf3f846a..9e97a8eb58aa 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -21,9 +21,7 @@ import android.annotation.NonNull; import android.annotation.TestApi; import android.annotation.UiContext; import android.app.Activity; -import android.app.ActivityThread; import android.app.AppGlobals; -import android.app.Application; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Configuration; @@ -41,13 +39,14 @@ import android.util.SparseArray; import android.util.TypedValue; import android.view.flags.Flags; -import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; /** * Contains methods to standard constants used in the UI for timeouts, sizes, and distances. */ public class ViewConfiguration { + private static final String TAG = "ViewConfiguration"; + /** * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in * dips @@ -350,8 +349,6 @@ public class ViewConfiguration { */ private static final int SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND = 500; - private static ResourceCache sResourceCache = new ResourceCache(); - private final boolean mConstructedWithContext; private final int mEdgeSlop; private final int mFadingEdgeLength; @@ -377,6 +374,7 @@ public class ViewConfiguration { private final int mOverscrollDistance; private final int mOverflingDistance; private final boolean mViewTouchScreenHapticScrollFeedbackEnabled; + @UnsupportedAppUsage private final boolean mFadingMarqueeEnabled; private final long mGlobalActionsKeyTimeout; private final float mVerticalScrollFactor; @@ -470,12 +468,14 @@ public class ViewConfiguration { mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f); mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f); - mScrollbarSize = res.getDimensionPixelSize(R.dimen.config_scrollbarSize); + mScrollbarSize = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_scrollbarSize); mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f); mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f); final TypedValue multiplierValue = new TypedValue(); - res.getValue(R.dimen.config_ambiguousGestureMultiplier, + res.getValue( + com.android.internal.R.dimen.config_ambiguousGestureMultiplier, multiplierValue, true /*resolveRefs*/); mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat()); @@ -488,7 +488,8 @@ public class ViewConfiguration { mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f); if (!sHasPermanentMenuKeySet) { - final int configVal = res.getInteger(R.integer.config_overrideHasPermanentMenuKey); + final int configVal = res.getInteger( + com.android.internal.R.integer.config_overrideHasPermanentMenuKey); switch (configVal) { default: @@ -515,27 +516,32 @@ public class ViewConfiguration { } } - mFadingMarqueeEnabled = res.getBoolean(R.bool.config_ui_enableFadingMarquee); - mTouchSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationTouchSlop); + mFadingMarqueeEnabled = res.getBoolean( + com.android.internal.R.bool.config_ui_enableFadingMarquee); + mTouchSlop = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_viewConfigurationTouchSlop); mHandwritingSlop = res.getDimensionPixelSize( - R.dimen.config_viewConfigurationHandwritingSlop); - mHoverSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationHoverSlop); + com.android.internal.R.dimen.config_viewConfigurationHandwritingSlop); + mHoverSlop = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_viewConfigurationHoverSlop); mMinScrollbarTouchTarget = res.getDimensionPixelSize( - R.dimen.config_minScrollbarTouchTarget); + com.android.internal.R.dimen.config_minScrollbarTouchTarget); mPagingTouchSlop = mTouchSlop * 2; mDoubleTapTouchSlop = mTouchSlop; mHandwritingGestureLineMargin = res.getDimensionPixelSize( - R.dimen.config_viewConfigurationHandwritingGestureLineMargin); + com.android.internal.R.dimen.config_viewConfigurationHandwritingGestureLineMargin); - mMinimumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMinFlingVelocity); - mMaximumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMaxFlingVelocity); + mMinimumFlingVelocity = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_viewMinFlingVelocity); + mMaximumFlingVelocity = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_viewMaxFlingVelocity); int configMinRotaryEncoderFlingVelocity = res.getDimensionPixelSize( - R.dimen.config_viewMinRotaryEncoderFlingVelocity); + com.android.internal.R.dimen.config_viewMinRotaryEncoderFlingVelocity); int configMaxRotaryEncoderFlingVelocity = res.getDimensionPixelSize( - R.dimen.config_viewMaxRotaryEncoderFlingVelocity); + com.android.internal.R.dimen.config_viewMaxRotaryEncoderFlingVelocity); if (configMinRotaryEncoderFlingVelocity < 0 || configMaxRotaryEncoderFlingVelocity < 0) { mMinimumRotaryEncoderFlingVelocity = NO_FLING_MIN_VELOCITY; mMaximumRotaryEncoderFlingVelocity = NO_FLING_MAX_VELOCITY; @@ -545,7 +551,8 @@ public class ViewConfiguration { } int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels = - res.getDimensionPixelSize(R.dimen + res.getDimensionPixelSize( + com.android.internal.R.dimen .config_rotaryEncoderAxisScrollTickInterval); mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0 @@ -553,31 +560,41 @@ public class ViewConfiguration { : NO_HAPTIC_SCROLL_TICK_INTERVAL; mRotaryEncoderHapticScrollFeedbackEnabled = - res.getBoolean(R.bool + res.getBoolean( + com.android.internal.R.bool .config_viewRotaryEncoderHapticScrollFedbackEnabled); - mGlobalActionsKeyTimeout = res.getInteger(R.integer.config_globalActionsKeyTimeout); + mGlobalActionsKeyTimeout = res.getInteger( + com.android.internal.R.integer.config_globalActionsKeyTimeout); - mHorizontalScrollFactor = res.getDimensionPixelSize(R.dimen.config_horizontalScrollFactor); - mVerticalScrollFactor = res.getDimensionPixelSize(R.dimen.config_verticalScrollFactor); + mHorizontalScrollFactor = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_horizontalScrollFactor); + mVerticalScrollFactor = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_verticalScrollFactor); mShowMenuShortcutsWhenKeyboardPresent = res.getBoolean( - R.bool.config_showMenuShortcutsWhenKeyboardPresent); + com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent); - mMinScalingSpan = res.getDimensionPixelSize(R.dimen.config_minScalingSpan); + mMinScalingSpan = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_minScalingSpan); - mScreenshotChordKeyTimeout = res.getInteger(R.integer.config_screenshotChordKeyTimeout); + mScreenshotChordKeyTimeout = res.getInteger( + com.android.internal.R.integer.config_screenshotChordKeyTimeout); mSmartSelectionInitializedTimeout = res.getInteger( - R.integer.config_smartSelectionInitializedTimeoutMillis); + com.android.internal.R.integer.config_smartSelectionInitializedTimeoutMillis); mSmartSelectionInitializingTimeout = res.getInteger( - R.integer.config_smartSelectionInitializingTimeoutMillis); - mPreferKeepClearForFocusEnabled = res.getBoolean(R.bool.config_preferKeepClearForFocus); + com.android.internal.R.integer.config_smartSelectionInitializingTimeoutMillis); + mPreferKeepClearForFocusEnabled = res.getBoolean( + com.android.internal.R.bool.config_preferKeepClearForFocus); mViewBasedRotaryEncoderScrollHapticsEnabledConfig = - res.getBoolean(R.bool.config_viewBasedRotaryEncoderHapticsEnabled); + res.getBoolean( + com.android.internal.R.bool.config_viewBasedRotaryEncoderHapticsEnabled); mViewTouchScreenHapticScrollFeedbackEnabled = Flags.enableScrollFeedbackForTouch() - ? res.getBoolean(R.bool.config_viewTouchScreenHapticScrollFeedbackEnabled) + ? res.getBoolean( + com.android.internal.R.bool + .config_viewTouchScreenHapticScrollFeedbackEnabled) : false; } @@ -615,7 +632,6 @@ public class ViewConfiguration { @VisibleForTesting public static void resetCacheForTesting() { sConfigurations.clear(); - sResourceCache = new ResourceCache(); } /** @@ -691,7 +707,7 @@ public class ViewConfiguration { * components. */ public static int getPressedStateDuration() { - return sResourceCache.getPressedStateDuration(); + return PRESSED_STATE_DURATION; } /** @@ -736,7 +752,7 @@ public class ViewConfiguration { * considered to be a tap. */ public static int getTapTimeout() { - return sResourceCache.getTapTimeout(); + return TAP_TIMEOUT; } /** @@ -745,7 +761,7 @@ public class ViewConfiguration { * considered to be a tap. */ public static int getJumpTapTimeout() { - return sResourceCache.getJumpTapTimeout(); + return JUMP_TAP_TIMEOUT; } /** @@ -754,7 +770,7 @@ public class ViewConfiguration { * double-tap. */ public static int getDoubleTapTimeout() { - return sResourceCache.getDoubleTapTimeout(); + return DOUBLE_TAP_TIMEOUT; } /** @@ -766,7 +782,7 @@ public class ViewConfiguration { */ @UnsupportedAppUsage public static int getDoubleTapMinTime() { - return sResourceCache.getDoubleTapMinTime(); + return DOUBLE_TAP_MIN_TIME; } /** @@ -776,7 +792,7 @@ public class ViewConfiguration { * @hide */ public static int getHoverTapTimeout() { - return sResourceCache.getHoverTapTimeout(); + return HOVER_TAP_TIMEOUT; } /** @@ -787,7 +803,7 @@ public class ViewConfiguration { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static int getHoverTapSlop() { - return sResourceCache.getHoverTapSlop(); + return HOVER_TAP_SLOP; } /** @@ -1028,7 +1044,7 @@ public class ViewConfiguration { * in milliseconds. */ public static long getZoomControlsTimeout() { - return sResourceCache.getZoomControlsTimeout(); + return ZOOM_CONTROLS_TIMEOUT; } /** @@ -1097,14 +1113,14 @@ public class ViewConfiguration { * friction. */ public static float getScrollFriction() { - return sResourceCache.getScrollFriction(); + return SCROLL_FRICTION; } /** * @return the default duration in milliseconds for {@link ActionMode#hide(long)}. */ public static long getDefaultActionModeHideDuration() { - return sResourceCache.getDefaultActionModeHideDuration(); + return ACTION_MODE_HIDE_DURATION_DEFAULT; } /** @@ -1455,137 +1471,8 @@ public class ViewConfiguration { return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT; } - private static int getDisplayDensity(Context context) { + private static final int getDisplayDensity(Context context) { final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); return (int) (100.0f * metrics.density); } - - /** - * Fetches resource values statically and caches them locally for fast lookup. Note that these - * values will not be updated during the lifetime of a process, even if resource overlays are - * applied. - */ - private static final class ResourceCache { - - private int mPressedStateDuration = -1; - private int mTapTimeout = -1; - private int mJumpTapTimeout = -1; - private int mDoubleTapTimeout = -1; - private int mDoubleTapMinTime = -1; - private int mHoverTapTimeout = -1; - private int mHoverTapSlop = -1; - private long mZoomControlsTimeout = -1L; - private float mScrollFriction = -1f; - private long mDefaultActionModeHideDuration = -1L; - - public int getPressedStateDuration() { - if (mPressedStateDuration < 0) { - Resources resources = getCurrentResources(); - mPressedStateDuration = resources != null - ? resources.getInteger(R.integer.config_pressedStateDurationMillis) - : PRESSED_STATE_DURATION; - } - return mPressedStateDuration; - } - - public int getTapTimeout() { - if (mTapTimeout < 0) { - Resources resources = getCurrentResources(); - mTapTimeout = resources != null - ? resources.getInteger(R.integer.config_tapTimeoutMillis) - : TAP_TIMEOUT; - } - return mTapTimeout; - } - - public int getJumpTapTimeout() { - if (mJumpTapTimeout < 0) { - Resources resources = getCurrentResources(); - mJumpTapTimeout = resources != null - ? resources.getInteger(R.integer.config_jumpTapTimeoutMillis) - : JUMP_TAP_TIMEOUT; - } - return mJumpTapTimeout; - } - - public int getDoubleTapTimeout() { - if (mDoubleTapTimeout < 0) { - Resources resources = getCurrentResources(); - mDoubleTapTimeout = resources != null - ? resources.getInteger(R.integer.config_doubleTapTimeoutMillis) - : DOUBLE_TAP_TIMEOUT; - } - return mDoubleTapTimeout; - } - - public int getDoubleTapMinTime() { - if (mDoubleTapMinTime < 0) { - Resources resources = getCurrentResources(); - mDoubleTapMinTime = resources != null - ? resources.getInteger(R.integer.config_doubleTapMinTimeMillis) - : DOUBLE_TAP_MIN_TIME; - } - return mDoubleTapMinTime; - } - - public int getHoverTapTimeout() { - if (mHoverTapTimeout < 0) { - Resources resources = getCurrentResources(); - mHoverTapTimeout = resources != null - ? resources.getInteger(R.integer.config_hoverTapTimeoutMillis) - : HOVER_TAP_TIMEOUT; - } - return mHoverTapTimeout; - } - - public int getHoverTapSlop() { - if (mHoverTapSlop < 0) { - Resources resources = getCurrentResources(); - mHoverTapSlop = resources != null - ? resources.getDimensionPixelSize(R.dimen.config_hoverTapSlop) - : HOVER_TAP_SLOP; - } - return mHoverTapSlop; - } - - public long getZoomControlsTimeout() { - if (mZoomControlsTimeout < 0) { - Resources resources = getCurrentResources(); - mZoomControlsTimeout = resources != null - ? resources.getInteger(R.integer.config_zoomControlsTimeoutMillis) - : ZOOM_CONTROLS_TIMEOUT; - } - return mZoomControlsTimeout; - } - - public float getScrollFriction() { - if (mScrollFriction < 0) { - Resources resources = getCurrentResources(); - mScrollFriction = resources != null - ? resources.getFloat(R.dimen.config_scrollFriction) - : SCROLL_FRICTION; - } - return mScrollFriction; - } - - public long getDefaultActionModeHideDuration() { - if (mDefaultActionModeHideDuration < 0) { - Resources resources = getCurrentResources(); - mDefaultActionModeHideDuration = resources != null - ? resources.getInteger(R.integer.config_defaultActionModeHideDurationMillis) - : ACTION_MODE_HIDE_DURATION_DEFAULT; - } - return mDefaultActionModeHideDuration; - } - - private static Resources getCurrentResources() { - if (!android.companion.virtualdevice.flags.Flags - .migrateViewconfigurationConstantsToResources()) { - return null; - } - Application application = ActivityThread.currentApplication(); - Context context = application != null ? application.getApplicationContext() : null; - return context != null ? context.getResources() : null; - } - } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 900f22d2b37b..0d6f82773622 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -133,6 +133,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; +import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateOnConfigChange; import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -271,7 +272,9 @@ import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.ScreenCapture; import android.window.SurfaceSyncGroup; +import android.window.WindowContext; import android.window.WindowOnBackInvokedDispatcher; +import android.window.WindowTokenClient; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -6609,12 +6612,26 @@ public final class ViewRootImpl implements ViewParent, mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId, activityWindowInfo); } else { - // There is no activity callback - update the configuration right away. + if (enableWindowContextResourcesUpdateOnConfigChange()) { + // There is no activity callback - update resources for window token, if needed. + final WindowTokenClient windowTokenClient = getWindowTokenClient(); + if (windowTokenClient != null) { + windowTokenClient.onConfigurationChanged( + mLastReportedMergedConfiguration.getMergedConfiguration(), + newDisplayId == INVALID_DISPLAY ? mDisplay.getDisplayId() + : newDisplayId); + } + } updateConfiguration(newDisplayId); } mForceNextConfigUpdate = false; } + private WindowTokenClient getWindowTokenClient() { + if (!(mContext instanceof WindowContext)) return null; + return (WindowTokenClient) mContext.getWindowContextToken(); + } + /** * Update display and views if last applied merged configuration changed. * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise. diff --git a/core/java/android/view/accessibility/OWNERS b/core/java/android/view/accessibility/OWNERS index f62b33f1f753..799ef0091f71 100644 --- a/core/java/android/view/accessibility/OWNERS +++ b/core/java/android/view/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0fb80422833c..56f0415b40cc 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -3778,8 +3778,32 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); if (Flags.refactorInsetsController()) { - mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), - false /* fromIme */, statsToken); + synchronized (mH) { + Handler vh = rootView.getHandler(); + if (vh == null) { + // If the view doesn't have a handler, something has changed out from + // under us. + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + return; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + + if (vh.getLooper() != Looper.myLooper()) { + // The view is running on a different thread than our own, so + // we need to reschedule our work for over there. + if (DEBUG) { + Log.v(TAG, "Close current input: reschedule hide to view thread"); + } + final var viewRootImpl = mCurRootView; + vh.post(() -> viewRootImpl.getInsetsController().hide( + WindowInsets.Type.ime(), false /* fromIme */, statsToken)); + } else { + mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), + false /* fromIme */, statsToken); + } + } } else { IInputMethodManagerGlobalInvoker.hideSoftInput( mClient, diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 785246074cee..1ce5df7cd137 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -55,6 +55,7 @@ public enum DesktopModeFlags { Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix, true), + ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false), ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( Flags::enableDesktopRecentsTransitionsCornersBugfix, false), diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 512113692c76..cf21e50e0a19 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -1291,12 +1291,13 @@ public final class TransitionInfo implements Parcelable { return options; } - /** Make options for a scale-up animation. */ + /** Make options for a scale-up animation with task override option */ @NonNull public static AnimationOptions makeScaleUpAnimOptions(int startX, int startY, int width, - int height) { + int height, boolean overrideTaskTransition) { AnimationOptions options = new AnimationOptions(ANIM_SCALE_UP); options.mTransitionBounds.set(startX, startY, startX + width, startY + height); + options.mOverrideTaskTransition = overrideTaskTransition; return options; } diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index a551fe701c5b..f7bee619bc4b 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -106,7 +106,6 @@ public class WindowTokenClient extends Binder { * @param newConfig the updated {@link Configuration} * @param newDisplayId the updated {@link android.view.Display} ID */ - @VisibleForTesting(visibility = PACKAGE) @MainThread public void onConfigurationChanged(Configuration newConfig, int newDisplayId) { onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */); diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 09c6dc0e2b20..b805ac560b8d 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -677,6 +677,17 @@ flag { } flag { + name: "enable_window_context_resources_update_on_config_change" + namespace: "lse_desktop_experience" + description: "Updates window context resources before the view receives the config change callback." + bug: "394527409" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_desktop_tab_tearing_minimize_animation_bugfix" namespace: "lse_desktop_experience" description: "Enabling a minimize animation when a new window is opened via tab tearing and the Desktop Windowing open windows limit is reached." @@ -684,4 +695,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "enable_desktop_close_shortcut_bugfix" + namespace: "lse_desktop_experience" + description: "Fix the window-close keyboard shortcut in Desktop Mode." + bug: "394599430" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/accessibility/OWNERS b/core/java/com/android/internal/accessibility/OWNERS index 1265dfa2c441..dac64f47ba7e 100644 --- a/core/java/com/android/internal/accessibility/OWNERS +++ b/core/java/com/android/internal/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS
\ No newline at end of file diff --git a/core/java/com/android/internal/graphics/palette/OWNERS b/core/java/com/android/internal/graphics/palette/OWNERS index 731dca9b128f..df867252c01c 100644 --- a/core/java/com/android/internal/graphics/palette/OWNERS +++ b/core/java/com/android/internal/graphics/palette/OWNERS @@ -1,3 +1,2 @@ -# Bug component: 484670
-dupin@google.com
-jamesoleary@google.com
\ No newline at end of file +# Bug component: 484670 +dupin@google.com diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 05a33fe830e8..d8cf258e23ba 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -160,19 +160,21 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen Objects.requireNonNull(mConfigurationService, "A null ProtoLog Configuration Service was provided!"); - try { - var args = createConfigurationServiceRegisterClientArgs(); + mBackgroundLoggingService.execute(() -> { + try { + var args = createConfigurationServiceRegisterClientArgs(); - final var groupArgs = mLogGroups.values().stream() - .map(group -> new RegisterClientArgs - .GroupConfig(group.name(), group.isLogToLogcat())) - .toArray(RegisterClientArgs.GroupConfig[]::new); - args.setGroups(groupArgs); + final var groupArgs = mLogGroups.values().stream() + .map(group -> new RegisterClientArgs + .GroupConfig(group.name(), group.isLogToLogcat())) + .toArray(RegisterClientArgs.GroupConfig[]::new); + args.setGroups(groupArgs); - mConfigurationService.registerClient(this, args); - } catch (RemoteException e) { - throw new RuntimeException("Failed to register ProtoLog client"); - } + mConfigurationService.registerClient(this, args); + } catch (RemoteException e) { + throw new RuntimeException("Failed to register ProtoLog client"); + } + }); } /** diff --git a/core/java/com/android/internal/protolog/WmProtoLogGroups.java b/core/java/com/android/internal/protolog/WmProtoLogGroups.java index 4bd5d24b71e2..5edc2fbd4c8f 100644 --- a/core/java/com/android/internal/protolog/WmProtoLogGroups.java +++ b/core/java/com/android/internal/protolog/WmProtoLogGroups.java @@ -100,6 +100,8 @@ public enum WmProtoLogGroups implements IProtoLogGroup { WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), WM_DEBUG_EMBEDDED_WINDOWS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), + WM_DEBUG_PRESENTATION(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index d8f1b626abf2..31b9fd1ad170 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -284,6 +284,8 @@ void NativeDisplayEventReceiver::dispatchModeRejected(PhysicalDisplayId displayI displayId.value, modeId); ALOGV("receiver %p ~ Returned from Mode Rejected handler.", this); } + + mMessageQueue->raiseAndClearException(env, "dispatchModeRejected"); } void NativeDisplayEventReceiver::dispatchFrameRateOverrides( @@ -314,7 +316,7 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides( ALOGV("receiver %p ~ Returned from FrameRateOverride handler.", this); } - mMessageQueue->raiseAndClearException(env, "dispatchModeChanged"); + mMessageQueue->raiseAndClearException(env, "dispatchFrameRateOverrides"); } void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c9f4cdc8e3ce..51049889ecd6 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -9385,6 +9385,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.security.UpdateCertificateRevocationStatusJobService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader" android:exported="false"> <intent-filter> diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index 4ff3f8825cc4..ef5875eff06f 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -110,4 +110,8 @@ tap power gesture from triggering the selected target action. --> <integer name="config_doubleTapPowerGestureMode">0</integer> + + <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This + config enables OEMs to support its usage across tasks.--> + <bool name="config_enableCrossTaskScaleUpAnimation">true</bool> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 6d57427ce221..17acf9aed278 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3061,43 +3061,6 @@ {@link MotionEvent#ACTION_SCROLL} event. --> <dimen name="config_scrollFactor">64dp</dimen> - <!-- Duration in milliseconds of the pressed state in child components. --> - <integer name="config_pressedStateDurationMillis">64</integer> - - <!-- Duration in milliseconds we will wait to see if a touch event is a tap or a scroll. - If the user does not move within this interval, it is considered to be a tap. --> - <integer name="config_tapTimeoutMillis">100</integer> - - <!-- Duration in milliseconds we will wait to see if a touch event is a jump tap. - If the user does not move within this interval, it is considered to be a tap. --> - <integer name="config_jumpTapTimeoutMillis">500</integer> - - <!-- Duration in milliseconds between the first tap's up event and the second tap's down - event for an interaction to be considered a double-tap. --> - <integer name="config_doubleTapTimeoutMillis">300</integer> - - <!-- Minimum duration in milliseconds between the first tap's up event and the second tap's - down event for an interaction to be considered a double-tap. --> - <integer name="config_doubleTapMinTimeMillis">40</integer> - - <!-- Maximum duration in milliseconds between a touch pad touch and release for a given touch - to be considered a tap (click) as opposed to a hover movement gesture. --> - <integer name="config_hoverTapTimeoutMillis">150</integer> - - <!-- The amount of time in milliseconds that the zoom controls should be displayed on the - screen. --> - <integer name="config_zoomControlsTimeoutMillis">3000</integer> - - <!-- Default duration in milliseconds for {@link ActionMode#hide(long)}. --> - <integer name="config_defaultActionModeHideDurationMillis">2000</integer> - - <!-- Maximum distance in pixels that a touch pad touch can move before being released - for it to be considered a tap (click) as opposed to a hover movement gesture. --> - <dimen name="config_hoverTapSlop">20px</dimen> - - <!-- The amount of friction applied to scrolls and flings. --> - <item name="config_scrollFriction" format="float" type="dimen">0.015</item> - <!-- Maximum number of grid columns permitted in the ResolverActivity used for picking activities to handle an intent. --> <integer name="config_maxResolverActivityColumns">3</integer> @@ -7293,6 +7256,9 @@ <!-- Wear devices: An intent action that is used for remote intent. --> <string name="config_wearRemoteIntentAction" translatable="false" /> + <!-- Whether the current device's internal display can host desktop sessions. --> + <bool name="config_canInternalDisplayHostDesktops">false</bool> + <!-- Whether desktop mode is supported on the current device --> <bool name="config_isDesktopModeSupported">false</bool> @@ -7385,4 +7351,23 @@ <!-- Array containing the notification assistant service adjustments that are not supported by default on this device--> <string-array translatable="false" name="config_notificationDefaultUnsupportedAdjustments" /> + + <!-- Preference name of bugreport--> + <string name="prefs_bugreport" translatable="false">bugreports</string> + + <!-- key value of warning state stored in bugreport preference--> + <string name="key_warning_state" translatable="false">warning-state</string> + + <!-- Bugreport warning dialog state unknown--> + <integer name="bugreport_state_unknown">0</integer> + + <!-- Bugreport warning dialog state shows the warning dialog--> + <integer name="bugreport_state_show">1</integer> + + <!-- Bugreport warning dialog state skips the warning dialog--> + <integer name="bugreport_state_hide">2</integer> + + <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This + config enables OEMs to support its usage across tasks.--> + <bool name="config_enableCrossTaskScaleUpAnimation">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b80d1297f9d3..cc2897a2779e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4155,17 +4155,6 @@ <java-symbol type="string" name="config_headlineFontFamily" /> <java-symbol type="string" name="config_headlineFontFamilyMedium" /> - <java-symbol type="integer" name="config_pressedStateDurationMillis" /> - <java-symbol type="integer" name="config_tapTimeoutMillis" /> - <java-symbol type="integer" name="config_jumpTapTimeoutMillis" /> - <java-symbol type="integer" name="config_doubleTapTimeoutMillis" /> - <java-symbol type="integer" name="config_doubleTapMinTimeMillis" /> - <java-symbol type="integer" name="config_hoverTapTimeoutMillis" /> - <java-symbol type="integer" name="config_zoomControlsTimeoutMillis" /> - <java-symbol type="integer" name="config_defaultActionModeHideDurationMillis" /> - <java-symbol type="dimen" name="config_hoverTapSlop" /> - <java-symbol type="dimen" name="config_scrollFriction" /> - <java-symbol type="drawable" name="stat_sys_vitals" /> <java-symbol type="color" name="text_color_primary" /> @@ -5765,6 +5754,9 @@ <!-- Whether the developer option for desktop mode is supported on the current device --> <java-symbol type="bool" name="config_isDesktopModeDevOptionSupported" /> + <!-- Whether the current device's internal display can host desktop sessions. --> + <java-symbol type="bool" name="config_canInternalDisplayHostDesktops" /> + <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. --> <java-symbol type="integer" name="config_maxDesktopWindowingActiveTasks"/> @@ -5913,4 +5905,14 @@ <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" /> <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_title" /> <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_text" /> + + <java-symbol type="string" name="prefs_bugreport" /> + <java-symbol type="string" name="key_warning_state" /> + <java-symbol type="integer" name="bugreport_state_unknown" /> + <java-symbol type="integer" name="bugreport_state_show" /> + <java-symbol type="integer" name="bugreport_state_hide" /> + + <!-- Enable OEMs to support scale up anim across tasks.--> + <java-symbol type="bool" name="config_enableCrossTaskScaleUpAnimation" /> + </resources> diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 8fa510381060..dc2f0a69375d 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -307,8 +307,10 @@ public class DisplayManagerGlobalTest { assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, mDisplayManagerGlobal .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_ADDED, 0)); - assertEquals(DISPLAY_CHANGE_EVENTS, mDisplayManagerGlobal - .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, 0)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED, + mDisplayManagerGlobal + .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, + 0)); assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, mDisplayManagerGlobal.mapFiltersToInternalEventFlag( DisplayManager.EVENT_TYPE_DISPLAY_REMOVED, 0)); diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index dfded7321b2c..0c4ea79dd5be 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -102,6 +102,10 @@ public final class Bitmap implements Parcelable { private static volatile int sDefaultDensity = -1; + /** + * This id is not authoritative and can be duplicated if an ashmem bitmap is decoded from a + * parcel. + */ private long mId; /** diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 13d0169c47c5..a08f88a5b937 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -177,3 +177,10 @@ flag { description: "Factor task-view state tracking out of taskviewtransitions" bug: "384976265" } + +flag { + name: "enable_bubble_bar_on_phones" + namespace: "multitasking" + description: "Try out bubble bar on phones" + bug: "394869612" +} diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 0b1f76f5ce0e..d280083ae7f5 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -17,4 +17,23 @@ <resources> <dimen name="floating_dismiss_icon_size">32dp</dimen> <dimen name="floating_dismiss_background_size">96dp</dimen> + + <!-- Bubble drag zone dimensions --> + <dimen name="drag_zone_dismiss_fold">140dp</dimen> + <dimen name="drag_zone_dismiss_tablet">200dp</dimen> + <dimen name="drag_zone_bubble_fold">140dp</dimen> + <dimen name="drag_zone_bubble_tablet">200dp</dimen> + <dimen name="drag_zone_full_screen_width">512dp</dimen> + <dimen name="drag_zone_full_screen_height">44dp</dimen> + <dimen name="drag_zone_desktop_window_width">880dp</dimen> + <dimen name="drag_zone_desktop_window_height">300dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_height">350dp</dimen> + <dimen name="drag_zone_split_from_bubble_height">100dp</dimen> + <dimen name="drag_zone_split_from_bubble_width">60dp</dimen> + <dimen name="drag_zone_h_split_from_expanded_view_width">60dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> </resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index aa523f57c469..909e9d2c4428 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -16,11 +16,15 @@ package com.android.wm.shell.shared.bubbles +import android.content.Context import android.graphics.Rect +import androidx.annotation.DimenRes +import com.android.wm.shell.shared.R import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode /** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */ class DragZoneFactory( + private val context: Context, private val deviceConfig: DeviceConfig, private val splitScreenModeChecker: SplitScreenModeChecker, private val desktopWindowModeChecker: DesktopWindowModeChecker, @@ -29,23 +33,65 @@ class DragZoneFactory( private val windowBounds: Rect get() = deviceConfig.windowBounds - // TODO b/393172431: move these to xml - private val dismissDragZoneSize = if (deviceConfig.isSmallTablet) 140 else 200 - private val bubbleDragZoneTabletSize = 200 - private val bubbleDragZoneFoldableSize = 140 - private val fullScreenDragZoneWidth = 512 - private val fullScreenDragZoneHeight = 44 - private val desktopWindowDragZoneWidth = 880 - private val desktopWindowDragZoneHeight = 300 - private val desktopWindowFromExpandedViewDragZoneWidth = 200 - private val desktopWindowFromExpandedViewDragZoneHeight = 350 - private val splitFromBubbleDragZoneHeight = 100 - private val splitFromBubbleDragZoneWidth = 60 - private val hSplitFromExpandedViewDragZoneWidth = 60 - private val vSplitFromExpandedViewDragZoneWidth = 200 - private val vSplitFromExpandedViewDragZoneHeightTablet = 285 - private val vSplitFromExpandedViewDragZoneHeightFoldTall = 150 - private val vSplitFromExpandedViewDragZoneHeightFoldShort = 100 + private var dismissDragZoneSize = 0 + private var bubbleDragZoneTabletSize = 0 + private var bubbleDragZoneFoldableSize = 0 + private var fullScreenDragZoneWidth = 0 + private var fullScreenDragZoneHeight = 0 + private var desktopWindowDragZoneWidth = 0 + private var desktopWindowDragZoneHeight = 0 + private var desktopWindowFromExpandedViewDragZoneWidth = 0 + private var desktopWindowFromExpandedViewDragZoneHeight = 0 + private var splitFromBubbleDragZoneHeight = 0 + private var splitFromBubbleDragZoneWidth = 0 + private var hSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneHeightTablet = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0 + + init { + onConfigurationUpdated() + } + + /** Updates all dimensions after a configuration change. */ + fun onConfigurationUpdated() { + dismissDragZoneSize = + if (deviceConfig.isSmallTablet) { + context.resolveDimension(R.dimen.drag_zone_dismiss_fold) + } else { + context.resolveDimension(R.dimen.drag_zone_dismiss_tablet) + } + bubbleDragZoneTabletSize = context.resolveDimension(R.dimen.drag_zone_bubble_tablet) + bubbleDragZoneFoldableSize = context.resolveDimension(R.dimen.drag_zone_bubble_fold) + fullScreenDragZoneWidth = context.resolveDimension(R.dimen.drag_zone_full_screen_width) + fullScreenDragZoneHeight = context.resolveDimension(R.dimen.drag_zone_full_screen_height) + desktopWindowDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_width) + desktopWindowDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_height) + desktopWindowFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_width) + desktopWindowFromExpandedViewDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_height) + splitFromBubbleDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_height) + splitFromBubbleDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_width) + hSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_h_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneHeightTablet = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_tablet) + vSplitFromExpandedViewDragZoneHeightFoldTall = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall) + vSplitFromExpandedViewDragZoneHeightFoldShort = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short) + } + + private fun Context.resolveDimension(@DimenRes dimension: Int) = + resources.getDimensionPixelSize(dimension) /** * Creates the list of drag zones for the dragged object. @@ -58,11 +104,11 @@ class DragZoneFactory( when (draggedObject) { is DraggedObject.BubbleBar -> { dragZones.add(createDismissDragZone()) - dragZones.addAll(createBubbleDragZones()) + dragZones.addAll(createBubbleHalfScreenDragZones()) } is DraggedObject.Bubble -> { dragZones.add(createDismissDragZone()) - dragZones.addAll(createBubbleDragZones()) + dragZones.addAll(createBubbleCornerDragZones()) dragZones.add(createFullScreenDragZone()) if (shouldShowDesktopWindowDragZones()) { dragZones.add(createDesktopWindowDragZoneForBubble()) @@ -80,7 +126,7 @@ class DragZoneFactory( } else { dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet()) } - createBubbleDragZonesForExpandedView() + dragZones.addAll(createBubbleHalfScreenDragZones()) } } return dragZones @@ -98,7 +144,7 @@ class DragZoneFactory( ) } - private fun createBubbleDragZones(): List<DragZone> { + private fun createBubbleCornerDragZones(): List<DragZone> { val dragZoneSize = if (deviceConfig.isSmallTablet) { bubbleDragZoneFoldableSize @@ -124,7 +170,7 @@ class DragZoneFactory( ) } - private fun createBubbleDragZonesForExpandedView(): List<DragZone> { + private fun createBubbleHalfScreenDragZones(): List<DragZone> { return listOf( DragZone.Bubble.Left( bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom), diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt new file mode 100644 index 000000000000..29ce8d90e66f --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +/** + * Manages animating drop targets in response to dragging bubble icons or bubble expanded views + * across different drag zones. + */ +class DropTargetManager( + private val isLayoutRtl: Boolean, + private val dragZoneChangedListener: DragZoneChangedListener +) { + + private var state: DragState? = null + + /** Must be called when a drag gesture is starting. */ + fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) { + val state = DragState(dragZones, draggedObject) + dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone) + this.state = state + } + + /** Called when the user drags to a new location. */ + fun onDragUpdated(x: Int, y: Int) { + val state = state ?: return + val oldDragZone = state.currentDragZone + val newDragZone = state.getMatchingDragZone(x = x, y = y) + state.currentDragZone = newDragZone + if (oldDragZone != newDragZone) { + dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone) + } + } + + /** Called when the drag ended. */ + fun onDragEnded() { + state = null + } + + /** Stores the current drag state. */ + private inner class DragState( + private val dragZones: List<DragZone>, + draggedObject: DraggedObject + ) { + val initialDragZone = + if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) { + dragZones.filterIsInstance<DragZone.Bubble.Left>().first() + } else { + dragZones.filterIsInstance<DragZone.Bubble.Right>().first() + } + var currentDragZone: DragZone = initialDragZone + + fun getMatchingDragZone(x: Int, y: Int): DragZone { + return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone + } + } + + /** An interface to be notified when drag zones change. */ + interface DragZoneChangedListener { + /** An initial drag zone was set. Called when a drag starts. */ + fun onInitialDragZoneSet(dragZone: DragZone) + /** Called when the object was dragged to a different drag zone. */ + fun onDragZoneChanged(from: DragZone, to: DragZone) + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 2586bd6d86cb..643c1506e4c2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -220,6 +220,13 @@ public class DesktopModeStatus { } /** + * Return {@code true} if the current device can host desktop sessions on its internal display. + */ + public static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + + /** * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopModeDevOption(@NonNull Context context) { @@ -231,21 +238,24 @@ public class DesktopModeStatus { * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) { - return Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode(context); + return Flags.showDesktopExperienceDevOption() + && isInternalDisplayEligibleToHostDesktops(context); } /** Returns if desktop mode dev option should be enabled if there is no user override. */ public static boolean shouldDevOptionBeEnabledByDefault(Context context) { - return isDeviceEligibleForDesktopMode(context) && Flags.enableDesktopWindowingMode(); + return isInternalDisplayEligibleToHostDesktops(context) + && Flags.enableDesktopWindowingMode(); } /** * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - return (isDeviceEligibleForDesktopMode(context) - && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) - || isDesktopModeEnabledByDevOption(context); + return (isInternalDisplayEligibleToHostDesktops(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() + && (isDesktopModeSupported(context) || !enforceDeviceRestrictions()) + || isDesktopModeEnabledByDevOption(context)); } /** @@ -313,10 +323,11 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop mode is unrestricted and is supported in the device. + * Return {@code true} if desktop sessions is unrestricted and can be host for the device's + * internal display. */ - public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !enforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + public static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { + return !enforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported( context)); } @@ -325,7 +336,7 @@ public class DesktopModeStatus { * Return {@code true} if the developer option for desktop mode is unrestricted and is supported * in the device. * - * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then + * Note that, if {@link #isInternalDisplayEligibleToHostDesktops(Context)} is true, then * {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true. */ private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 8cf2370df48d..c7a0401c2b88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -793,15 +793,21 @@ public class BubbleController implements ConfigurationChangeListener, public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, @BubbleBarLocation.UpdateSource int source) { if (isShowingAsBubbleBar()) { + updateExpandedViewForBubbleBarLocation(bubbleBarLocation, source); + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; + mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); + } + } + + private void updateExpandedViewForBubbleBarLocation(BubbleBarLocation bubbleBarLocation, + @BubbleBarLocation.UpdateSource int source) { + if (isShowingAsBubbleBar()) { BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation(); mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { mLayerView.updateExpandedView(); } - BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); - bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; - mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); - logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source); } } @@ -874,7 +880,8 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent itemIntent) { + public void onItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location, + Intent itemIntent) { hideBubbleBarExpandedViewDropTarget(); ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent .getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO); @@ -1521,18 +1528,19 @@ public class BubbleController implements ConfigurationChangeListener, public void expandStackAndSelectBubble(ShortcutInfo info, @Nullable BubbleBarLocation bubbleBarLocation) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; - if (bubbleBarLocation != null) { - //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack & - // fix bubble bar flicking - setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); } Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, + updateLocation); } } @@ -1562,19 +1570,19 @@ public class BubbleController implements ConfigurationChangeListener, public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user, @Nullable BubbleBarLocation bubbleBarLocation) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; - if (bubbleBarLocation != null) { - //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack & - // fix bubble bar flicking - setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); } Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user); ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s", pendingIntent); if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, updateLocation); } } @@ -1940,11 +1948,22 @@ public class BubbleController implements ConfigurationChangeListener, @VisibleForTesting public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + inflateAndAdd(bubble, suppressFlyout, showInShade, /* bubbleBarLocation= */ null); + } + + /** + * Inflates and adds a bubble. Updates Bubble Bar location if bubbles + * are shown in the Bubble Bar and the location is not null. + */ + @VisibleForTesting + public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); bubble.setInflateSynchronously(mInflateSynchronously); bubble.inflate( - b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), + b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade, + bubbleBarLocation), mContext, mExpandedViewManager, mBubbleTaskViewFactory, @@ -2278,7 +2297,8 @@ public class BubbleController implements ConfigurationChangeListener, ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:" + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b" + " expanded=%b selectionChanged=%b selected=%s" - + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b", + + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b" + + " bubbleBarLocation=%s", update.addedBubble != null ? update.addedBubble.getKey() : "null", !update.removedBubbles.isEmpty(), update.updatedBubble != null ? update.updatedBubble.getKey() : "null", @@ -2287,7 +2307,9 @@ public class BubbleController implements ConfigurationChangeListener, update.selectedBubble != null ? update.selectedBubble.getKey() : "null", update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null", update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null", - update.shouldShowEducation, update.showOverflowChanged); + update.shouldShowEducation, update.showOverflowChanged, + update.mBubbleBarLocation != null ? update.mBubbleBarLocation.toString() + : "null"); ensureBubbleViewsAndWindowCreated(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index f97133a4c3d1..abcdb7e70cec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -43,6 +43,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.bubbles.RemovedBubble; @@ -91,6 +92,8 @@ public class BubbleData { @Nullable Bubble suppressedBubble; @Nullable Bubble unsuppressedBubble; @Nullable String suppressedSummaryGroup; + @Nullable + BubbleBarLocation mBubbleBarLocation; // Pair with Bubble and @DismissReason Integer final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); @@ -116,6 +119,7 @@ public class BubbleData { || unsuppressedBubble != null || suppressedSummaryChanged || suppressedSummaryGroup != null + || mBubbleBarLocation != null || showOverflowChanged; } @@ -169,6 +173,7 @@ public class BubbleData { } bubbleBarUpdate.showOverflowChanged = showOverflowChanged; bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty(); + bubbleBarUpdate.bubbleBarLocation = mBubbleBarLocation; return bubbleBarUpdate; } @@ -396,8 +401,23 @@ public class BubbleData { * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates. */ public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) { + setSelectedBubbleAndExpandStack(bubble, /* bubbleBarLocation = */ null); + } + + /** + * Sets the selected bubble and expands it. Also updates bubble bar location if the + * bubbleBarLocation is not {@code null} + * + * <p>This dispatches a single state update for 3 changes and should be used instead of + * calling {@link BubbleController#setBubbleBarLocation(BubbleBarLocation, int)} followed by + * {@link #setSelectedBubbleAndExpandStack(BubbleViewProvider)} immediately after, which will + * generate 2 separate updates. + */ + public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble, + @Nullable BubbleBarLocation bubbleBarLocation) { setSelectedBubbleInternal(bubble); setExpandedInternal(true); + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } @@ -513,13 +533,25 @@ public class BubbleData { } /** + * Calls {@link #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)} passing + * {@code null} for bubbleBarLocation. + * + * @see #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation) + */ + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + notificationEntryUpdated(bubble, suppressFlyout, showInShade, /* bubbleBarLocation = */ + null); + } + + /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager, * BubbleTaskViewFactory, BubblePositioner, BubbleLogger, BubbleStackView, * com.android.wm.shell.bubbles.bar.BubbleBarLayerView, * com.android.launcher3.icons.BubbleIconFactory, boolean) */ - void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); suppressFlyout |= !bubble.isTextChanged(); @@ -567,6 +599,7 @@ public class BubbleData { doSuppress(bubble); } } + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index aa42de67152a..e3b0872df593 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -524,8 +524,8 @@ public class BubbleBarLayerView extends FrameLayout * Skips logging if it is {@link BubbleOverflow}. */ private void logBubbleEvent(BubbleLogger.Event event) { - if (mExpandedBubble != null && mExpandedBubble instanceof Bubble bubble) { - mBubbleLogger.log(bubble, event); + if (mExpandedBubble != null && mExpandedBubble instanceof Bubble) { + mBubbleLogger.log((Bubble) mExpandedBubble, event); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index b2b99d648bf4..b6012378e4d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -914,12 +914,15 @@ public abstract class WMShellModule { Context context, Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + @DynamicOverride DesktopUserRepositories desktopUserRepositories, InteractionJankMonitor interactionJankMonitor) { return ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX.isTrue() ? new SpringDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor) + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor) : new DefaultDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor); + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor); } @WMSingleton 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 7491abd4248b..531304d6922a 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 @@ -1659,11 +1659,16 @@ class DesktopTasksController( private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { logV("addWallpaperActivity") if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { + + // If the wallpaper activity for this display already exists, let's reorder it to top. + val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId) + if (wallpaperActivityToken != null) { + wct.reorder(wallpaperActivityToken, /* onTop= */ true) + return + } + val intent = Intent(context, DesktopWallpaperActivity::class.java) - if ( - desktopWallpaperActivityTokenProvider.getToken(displayId) == null && - Flags.enablePerDisplayDesktopWallpaperActivity() - ) { + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt index a5ba6612bb1a..c10752d36bf9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt @@ -90,6 +90,11 @@ class DesktopUserRepositories( return desktopRepoByUserId.getOrCreate(profileId) } + fun getUserIdForProfile(profileId: Int): Int { + if (userIdToProfileIdsMap[userId]?.contains(profileId) == true) return userId + else return profileId + } + /** Dumps [DesktopRepository] for each user. */ fun dump(pw: PrintWriter, prefix: String) { desktopRepoByUserId.forEach { key, value -> 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 2ac76f319d32..8194d3cab445 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 @@ -70,6 +70,7 @@ sealed class DragToDesktopTransitionHandler( private val context: Context, private val transitions: Transitions, private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val desktopUserRepositories: DesktopUserRepositories, protected val interactionJankMonitor: InteractionJankMonitor, protected val transactionSupplier: Supplier<SurfaceControl.Transaction>, ) : TransitionHandler { @@ -127,15 +128,18 @@ sealed class DragToDesktopTransitionHandler( pendingIntentCreatorBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED } - val taskUser = UserHandle.of(taskInfo.userId) + // If we are launching home for a profile of a user, just use the [userId] of that user + // instead of the [profileId] to create the context. + val userToLaunchWith = + UserHandle.of(desktopUserRepositories.getUserIdForProfile(taskInfo.userId)) val pendingIntent = PendingIntent.getActivityAsUser( - context.createContextAsUser(taskUser, /* flags= */ 0), + context.createContextAsUser(userToLaunchWith, /* flags= */ 0), /* requestCode= */ 0, launchHomeIntent, FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT, options.toBundle(), - taskUser, + userToLaunchWith, ) val wct = WindowContainerTransaction() // The app that is being dragged into desktop mode might cause new transitions, make this @@ -881,6 +885,7 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() @@ -890,6 +895,7 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, transactionSupplier, ) { @@ -917,6 +923,7 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() @@ -926,6 +933,7 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, transactionSupplier, ) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index ae8f8c4eff79..7751741ae082 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -29,6 +29,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -46,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.Rect; @@ -73,6 +75,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.Flags; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; @@ -1353,6 +1356,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */); t.show(mPausingTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mPausingTasks); if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } @@ -1390,6 +1395,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, for (int i = 0; i < mOpeningTasks.size(); ++i) { t.show(mOpeningTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mOpeningTasks); for (int i = 0; i < mPausingTasks.size(); ++i) { cleanUpPausingOrClosingTask(mPausingTasks.get(i), wct, t, sendUserLeaveHint); } @@ -1450,6 +1457,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.clear(); if (Flags.enableRecentsBookendTransition()) { + // Notify the mixers of the pending finish + for (int i = 0; i < mMixers.size(); ++i) { + mMixers.get(i).handleFinishRecents(returningToApp, wct, t); + } + // In this case, we've already started the PIP transition, so we can // clean up immediately mPendingRunnerFinishCb = runnerFinishCb; @@ -1509,6 +1521,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } } + private static void setCornerRadiusForFreeformTasks( + Context context, + SurfaceControl.Transaction t, + ArrayList<TaskState> tasks) { + if (!ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) { + return; + } + int cornerRadius = getCornerRadius(context); + for (int i = 0; i < tasks.size(); ++i) { + TaskState task = tasks.get(i); + if (task.mTaskInfo != null && task.mTaskInfo.isFreeform()) { + t.setCornerRadius(task.mTaskSurface, cornerRadius); + } + } + } + + private static int getCornerRadius(Context context) { + return context.getResources().getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius); + } + private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) { if (tasks == null) { return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index aff21cbe0ae6..15ac03ccaf30 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1675,8 +1675,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct, @ExitReason int exitReason) { if (!isSplitActive()) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s", - stageTypeToString(stageToTop)); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s reason=%s", + stageTypeToString(stageToTop), exitReasonToString(exitReason)); if (enableFlexibleSplit()) { mStageOrderOperator.getActiveStages().stream() .filter(stage -> stage.getId() != stageToTop) @@ -3395,12 +3395,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, TransitionInfo.Change sideChild = null; StageTaskListener firstAppStage = null; StageTaskListener secondAppStage = null; + boolean foundPausingTask = false; final WindowContainerTransaction evictWct = new WindowContainerTransaction(); for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null || !taskInfo.hasParentTask()) continue; if (mPausingTasks.contains(taskInfo.taskId)) { + foundPausingTask = true; continue; } StageTaskListener stage = getStageOfTask(taskInfo); @@ -3443,9 +3445,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(dismissTop, cancelWct, EXIT_REASON_UNKNOWN); logExit(EXIT_REASON_UNKNOWN); }); - Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", - "launched 2 tasks in split, but didn't receive " - + "2 tasks in transition. Possibly one of them failed to launch")); + Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in " + + "split, but didn't receive 2 tasks in transition. Possibly one of them " + + "failed to launch (foundPausingTask=" + foundPausingTask + ")")); if (mRecentTasks.isPresent() && mainChild != null) { mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId); } @@ -3800,6 +3802,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Call this when the recents animation canceled during split-screen. */ public void onRecentsInSplitAnimationCanceled() { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationCanceled"); mPausingTasks.clear(); setSplitsVisible(false); @@ -3809,31 +3812,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer.applyTransaction(wct); } - public void onRecentsInSplitAnimationFinishing(boolean returnToApp, - @NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (!Flags.enableRecentsBookendTransition()) { - // The non-bookend recents transition case will be handled by - // RecentsMixedTransition wrapping the finish callback and calling - // onRecentsInSplitAnimationFinish() - return; - } - - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); - } - - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (Flags.enableRecentsBookendTransition()) { - // The bookend recents transition case will be handled by - // onRecentsInSplitAnimationFinishing above - return; - } - - // Check if the recent transition is finished by returning to the current - // split, so we can restore the divider bar. - boolean returnToApp = false; + /** + * Returns whether the given WCT is reordering any of the split tasks to top. + */ + public boolean wctIsReorderingSplitToTop(@NonNull WindowContainerTransaction finishWct) { for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { final WindowContainerTransaction.HierarchyOp op = finishWct.getHierarchyOps().get(i); @@ -3848,14 +3830,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() && anyStageContainsContainer) { - returnToApp = true; + return true; } } - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); + return false; } - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinishInner(boolean returnToApp, + /** Called when the recents animation during split-screen finishes. */ + public void onRecentsInSplitAnimationFinishing(boolean returnToApp, @NonNull WindowContainerTransaction finishWct, @NonNull SurfaceControl.Transaction finishT) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index f40dc8ad93b5..1e926c57ca61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -159,9 +159,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { // If pair-to-pair switching, the post-recents clean-up isn't needed. wct = wct != null ? wct : new WindowContainerTransaction(); if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { - // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove - // once that rolls out - mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); + // We've dispatched to the mLeftoversHandler to handle the rest of the transition + // and called onRecentsInSplitAnimationStart(), but if the recents handler is not + // actually handling the transition, then onRecentsInSplitAnimationFinishing() + // won't actually get called by the recents handler. In such cases, we still need + // to clean up after the changes from the start call. + boolean splitNotifiedByRecents = mRecentsHandler == mLeftoversHandler; + if (!splitNotifiedByRecents) { + mSplitHandler.onRecentsInSplitAnimationFinishing( + mSplitHandler.wctIsReorderingSplitToTop(wct), + wct, finishTransaction); + } } else { // notify pair-to-pair recents animation finish mSplitHandler.onRecentsPairToPairAnimationFinish(wct); diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 19829e7e5677..bac8e5062128 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -12,7 +12,6 @@ atsjenk@google.com jorgegil@google.com vaniadesmonda@google.com pbdr@google.com -tkachenkoi@google.com mpodolian@google.com jeremysim@google.com peanutbutter@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index 02b2cec8dbdb..ae73dae99d6f 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -53,10 +53,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index ffcc3446d436..7a7d88b80ce3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -572,6 +572,22 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.shouldShowEducation).isTrue(); } + /** Verifies that the update should contain the bubble bar location. */ + @Test + public void test_shouldUpdateBubbleBarLocation() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */ + true, BubbleBarLocation.LEFT); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.mBubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); + } + /** * Verifies that the update shouldn't show the user education, if the education is required but * the bubble should auto-expand @@ -1367,6 +1383,20 @@ public class BubbleDataTest extends ShellTestCase { } @Test + public void setSelectedBubbleAndExpandStackWithLocation() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1, BubbleBarLocation.LEFT); + + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + assertExpandedChangedTo(true); + assertLocationChangedTo(BubbleBarLocation.LEFT); + } + + @Test public void testShowOverflowChanged_hasOverflowBubbles() { assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); sendUpdatedEntryAtTime(mEntryA1, 1000); @@ -1450,6 +1480,12 @@ public class BubbleDataTest extends ShellTestCase { assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble); } + private void assertLocationChangedTo(BubbleBarLocation location) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("locationChanged").that(update.mBubbleBarLocation) + .isEqualTo(location); + } + private void assertExpandedChangedTo(boolean expected) { BubbleData.Update update = mUpdateCaptor.getValue(); assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index d6b13610c9c1..70a30a3ca7a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -113,7 +113,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt index 403d468a7034..d510570e8839 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt @@ -30,7 +30,6 @@ import android.view.KeyEvent import android.window.DisplayAreaInfo import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer -import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER @@ -48,7 +47,6 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction -import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel @@ -107,12 +105,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) - mockitoSession = - mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + mockitoSession = mockitoSession().strictness(Strictness.LENIENT).startMocking() testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) 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 cd1c16a93475..718bf322f6a9 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 @@ -171,7 +171,6 @@ import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock @@ -292,7 +291,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .spyStatic(DesktopModeStatus::class.java) .spyStatic(Toast::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) @@ -363,9 +362,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() shellInit.init() - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + val captor = argumentCaptor<RecentsTransitionStateListener>() verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value + recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener @@ -441,7 +440,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { val task1 = setUpFreeformTask() - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -461,7 +460,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() STABLE_BOUNDS.height(), displayController, ) - assertThat(argumentCaptor.value).isTrue() + assertThat(argumentCaptor.firstValue).isTrue() } @Test @@ -476,7 +475,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } val task1 = setUpFreeformTask(bounds = stableBounds, active = true) - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -497,7 +496,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(displayController), anyOrNull(), ) - assertThat(argumentCaptor.value).isFalse() + assertThat(argumentCaptor.firstValue).isFalse() } @Test @@ -547,6 +546,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -581,7 +581,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) // Wallpaper is moved to front. - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 0, wallpaperToken) // Desk is activated. verify(desksOrganizer).activateDesk(wct, deskId) } @@ -783,6 +783,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskVisible(task1) @@ -825,7 +826,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -842,6 +844,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() wct.assertReorderAt(index = 2, task2) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertReorderAt(index = 0, wallpaperToken) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + @Test @DisableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, @@ -860,9 +880,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { - whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) - .thenReturn(Binder()) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = @@ -871,10 +891,18 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, - ) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertReorderAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) @@ -899,6 +927,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) .thenReturn(Binder()) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) @@ -991,6 +1020,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() /** TODO: b/362720497 - add multi-desk version when minimization is implemented. */ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() @@ -1569,6 +1599,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) @@ -1736,7 +1767,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1751,12 +1782,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1768,7 +1799,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test @@ -1802,6 +1833,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) @@ -1828,6 +1860,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() { val freeformTask = setUpFreeformTask() @@ -1840,7 +1873,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) val wct = getLatestEnterDesktopWct() - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 0, wallpaperToken) verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask) verify(desksOrganizer).activateDesk(wct, deskId = 0) verify(desktopModeEnterExitTransitionListener) @@ -1967,6 +2000,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() @@ -2224,26 +2258,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun moveTaskToFront_remoteTransition_usesOneshotHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertThat(transitionHandlerArgCaptor.value) + assertThat(transitionHandlerArgCaptor.firstValue) .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) } @@ -2718,9 +2752,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2759,9 +2793,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2775,9 +2809,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } } @Test @@ -2791,10 +2825,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is being minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -2808,9 +2842,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is already minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2825,9 +2859,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2845,10 +2879,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // task1 is the only visible task as task2 is minimized. controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) // Adds remove wallpaper operation - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -2987,6 +3021,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -3118,6 +3153,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -3152,7 +3188,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) assertNotNull(result, "Should handle request") @@ -3180,6 +3218,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task createFreeformTask(displayId = SECOND_DISPLAY) @@ -4635,7 +4674,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4643,9 +4682,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(task2.configuration.windowConfiguration.bounds), ) - assertThat(wctArgument.value.hierarchyOps).hasSize(1) + assertThat(wctArgument.firstValue.hierarchyOps).hasSize(1) // Removes wallpaper activity when leaving desktop - wctArgument.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wctArgument.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -4660,7 +4699,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4669,7 +4708,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(task2.configuration.windowConfiguration.bounds), ) // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wctArgument.value.hierarchyOps).isEmpty() + assertThat(wctArgument.firstValue.hierarchyOps).isEmpty() } @Test @@ -4677,7 +4716,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromFullscreenOpensInSplit() { setUpLandscapeDisplay() val task = setUpFullscreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4690,7 +4729,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4699,7 +4738,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromSplitOpensInSplit() { setUpLandscapeDisplay() val task = setUpSplitScreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4712,7 +4751,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4807,11 +4846,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpFullscreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4821,11 +4860,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpSplitScreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -5912,35 +5951,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mockDragEvent, mockCallback as Consumer<Boolean>, ) - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() var expectedWindowingMode: Int if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { expectedWindowingMode = WINDOWING_MODE_FULLSCREEN // Fullscreen launches currently use default transitions - verify(transitions).startTransition(any(), capture(arg), anyOrNull()) + verify(transitions).startTransition(any(), arg.capture(), anyOrNull()) } else { expectedWindowingMode = WINDOWING_MODE_FREEFORM if (tabTearingAnimationFlagEnabled) { verify(desktopMixedTransitionHandler) .startLaunchTransition( eq(TRANSIT_OPEN), - capture(arg), + arg.capture(), anyOrNull(), anyOrNull(), anyOrNull(), ) } else { // All other launches use a special handler. - verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + verify(dragAndDropTransitionHandler).handleDropEvent(arg.capture()) } } assertThat( - ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) .launchWindowingMode ) .isEqualTo(expectedWindowingMode) - assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds) + assertThat( + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) + .launchBounds + ) .isEqualTo(expectedBounds) } @@ -6122,52 +6163,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out TransitionHandler>? = null, ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() if (handlerClass == null) { verify(transitions).startTransition(eq(type), arg.capture(), isNull()) } else { verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) } - return arg.value + return arg.lastValue } private fun getLatestToggleResizeDesktopTaskWct( currentBounds: Rect? = null ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(capture(arg), eq(currentBounds)) - return arg.value + .startTransition(arg.capture(), eq(currentBounds)) + return arg.lastValue } private fun getLatestDesktopMixedTaskWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(desktopMixedTransitionHandler) - .startLaunchTransition(eq(type), capture(arg), anyOrNull(), anyOrNull(), anyOrNull()) - return arg.value + .startLaunchTransition(eq(type), arg.capture(), anyOrNull(), anyOrNull(), anyOrNull()) + return arg.lastValue } private fun getLatestEnterDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) - return arg.value + return arg.lastValue } private fun getLatestDragToDesktopWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - return arg.value + val arg = argumentCaptor<WindowContainerTransaction>() + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(arg.capture()) + return arg.lastValue } private fun getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) - return arg.value + return arg.lastValue } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt index 83e48728c4f2..030bb1ace49d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt @@ -123,8 +123,26 @@ class DesktopUserRepositoriesTest : ShellTestCase() { assertThat(desktopRepository.userId).isEqualTo(PROFILE_ID_2) } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) + fun getUserForProfile_flagEnabled_returnsUserIdForProfile() { + userRepositories.onUserChanged(USER_ID_2, mock()) + val profiles: MutableList<UserInfo> = + mutableListOf( + UserInfo(USER_ID_2, "User profile", 0), + UserInfo(PROFILE_ID_1, "Work profile", 0), + ) + userRepositories.onUserProfilesChanged(profiles) + + val userIdForProfile = userRepositories.getUserIdForProfile(PROFILE_ID_1) + + assertThat(userIdForProfile).isEqualTo(USER_ID_2) + } + private companion object { const val USER_ID_1 = 7 + const val USER_ID_2 = 8 + const val PROFILE_ID_1 = 4 const val PROFILE_ID_2 = 5 } } 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 25246d9984c3..1732875f1d57 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 @@ -70,6 +70,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var draggedTaskLeash: SurfaceControl @Mock private lateinit var homeTaskLeash: SurfaceControl + @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -84,6 +85,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, transactionSupplier, ) @@ -93,6 +95,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, transactionSupplier, ) @@ -484,17 +487,22 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() - val startTransition = startDrag( - springHandler, task, finishTransaction = playingFinishTransaction, homeChange = null) + val startTransition = + startDrag( + springHandler, + task, + finishTransaction = playingFinishTransaction, + homeChange = null, + ) springHandler.onTaskResizeAnimationListener = mock() springHandler.mergeAnimation( transition = mock<IBinder>(), info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, - draggedTask = task, - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task, + ), startT = mergedStartTransaction, finishT = mergedFinishTransaction, mergeTarget = startTransition, @@ -723,7 +731,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun createTransitionInfo( type: Int, draggedTask: RunningTaskInfo, - homeChange: TransitionInfo.Change? = createHomeChange()) = + homeChange: TransitionInfo.Change? = createHomeChange(), + ) = TransitionInfo(type, /* flags= */ 0).apply { homeChange?.let { addChange(it) } addChange( // Dragged Task. @@ -741,11 +750,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } - private fun createHomeChange() = TransitionInfo.Change(mock(), homeTaskLeash).apply { - parent = null - taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() - flags = flags or FLAG_IS_WALLPAPER - } + private fun createHomeChange() = + TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + flags = flags or FLAG_IS_WALLPAPER + } private fun systemPropertiesKey(name: String) = "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index b50af741b2a6..439be9155b26 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -17,9 +17,13 @@ package com.android.wm.shell.recents; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED; @@ -44,9 +48,11 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.platform.test.annotations.EnableFlags; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -57,6 +63,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.os.IResultReceiver; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -92,9 +99,13 @@ import java.util.Optional; @SmallTest public class RecentsTransitionHandlerTest extends ShellTestCase { + private static final int FREEFORM_TASK_CORNER_RADIUS = 32; + @Mock private Context mContext; @Mock + private Resources mResources; + @Mock private TaskStackListenerImpl mTaskStackListener; @Mock private ShellCommandHandler mShellCommandHandler; @@ -134,6 +145,10 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); when(mContext.getSystemService(KeyguardManager.class)) .thenReturn(mock(KeyguardManager.class)); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius) + ).thenReturn(FREEFORM_TASK_CORNER_RADIUS); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, mDisplayInsetsController, mMainExecutor)); @@ -276,6 +291,57 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING); } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, freeformTask) + .build(); + SurfaceControl leash = mergeTransitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + transition, + mock(Transitions.TransitionFinishCallback.class)); + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testFinish_returningToFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo transitionInfo = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_CLOSE, freeformTask) + .build(); + SurfaceControl leash = transitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, transitionInfo, new StubTransaction(), finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + private IBinder startRecentsTransition(boolean synthetic) { return startRecentsTransition(synthetic, mock(IRecentsAnimationRunner.class)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt index e28d6ff8bf7f..fd22a84dee5d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt @@ -16,8 +16,10 @@ package com.android.wm.shell.shared.bubbles +import android.content.Context import android.graphics.Insets import android.graphics.Rect +import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker @@ -27,11 +29,14 @@ import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +private typealias DragZoneVerifier = (dragZone: DragZone) -> Unit + @SmallTest @RunWith(AndroidJUnit4::class) /** Unit tests for [DragZoneFactory]. */ class DragZoneFactoryTest { + private val context = getApplicationContext<Context>() private lateinit var dragZoneFactory: DragZoneFactory private val tabletPortrait = DeviceConfig( @@ -55,184 +60,238 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubbleBar_tablet() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble::class.java, - DragZone.Bubble::class.java, + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_tablet_portrait() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_tablet_landscape() { - dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_foldable_portrait() { - dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_foldable_landscape() { - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_tablet_portrait() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones( DraggedObject.ExpandedView(BubbleBarLocation.LEFT) ) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_tablet_landscape() { - dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_foldable_portrait() { - dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_foldable_landscape() { - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_tablet_desktopModeDisabled() { isDesktopWindowModeSupported = false - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() @@ -241,9 +300,21 @@ class DragZoneFactoryTest { @Test fun dragZonesForExpandedView_tablet_desktopModeDisabled() { isDesktopWindowModeSupported = false - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() } + + private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone -> + assertThat(dragZone).isInstanceOf(T::class.java) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt new file mode 100644 index 000000000000..efb91c5fbfda --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertFails + +/** Unit tests for [DropTargetManager]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DropTargetManagerTest { + + private lateinit var dropTargetManager: DropTargetManager + private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener + private val dropTarget = Rect(0, 0, 0, 0) + + // create 3 drop zones that are horizontally next to each other + // ------------------------------------------------- + // | | | | + // | bubble | | bubble | + // | | dismiss | | + // | left | | right | + // | | | | + // ------------------------------------------------- + private val bubbleLeftDragZone = + DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget) + private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100)) + private val bubbleRightDragZone = + DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget) + + @Before + fun setUp() { + dragZoneChangedListener = FakeDragZoneChangedListener() + dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener) + } + + @Test + fun onDragStarted_notifiesInitialDragZone() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + assertThat(dragZoneChangedListener.initialDragZone).isEqualTo(bubbleLeftDragZone) + } + + @Test + fun onDragStarted_missingExpectedDragZone_fails() { + assertFails { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.RIGHT), + listOf(bubbleLeftDragZone) + ) + } + } + + @Test + fun onDragUpdated_notifiesDragZoneChanged() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) + + dropTargetManager.onDragUpdated( + dismissDragZone.bounds.centerX(), + dismissDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) + } + + @Test + fun onDragUpdated_withinSameZone_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + dropTargetManager.onDragUpdated( + bubbleLeftDragZone.bounds.centerX(), + bubbleLeftDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + @Test + fun onDragUpdated_outsideAllZones_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + val pointX = 200 + val pointY = 200 + assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse() + assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse() + dropTargetManager.onDragUpdated(pointX, pointY) + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + @Test + fun onDragUpdated_hasOverlappingZones_notifiesFirstDragZoneChanged() { + // create a drag zone that spans across the width of all 3 drag zones, but extends below + // them + val splitDragZone = DragZone.Split.Left(bounds = Rect(0, 0, 300, 200)) + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone, splitDragZone) + ) + + // drag to a point that is within both the bubble right zone and split zone + val (pointX, pointY) = + Pair( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + assertThat(splitDragZone.contains(pointX, pointY)).isTrue() + dropTargetManager.onDragUpdated(pointX, pointY) + // verify we dragged to the bubble right zone because that has higher priority than split + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) + + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + 150 // below the bubble and dismiss drag zones but within split + ) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone) + + val (dismissPointX, dismissPointY) = + Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY()) + assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue() + dropTargetManager.onDragUpdated(dismissPointX, dismissPointY) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) + } + + @Test + fun onDragUpdated_afterDragEnded_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + dropTargetManager.onDragEnded() + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener { + var initialDragZone: DragZone? = null + var fromDragZone: DragZone? = null + var toDragZone: DragZone? = null + + override fun onInitialDragZoneSet(dragZone: DragZone) { + initialDragZone = dragZone + } + override fun onDragZoneChanged(from: DragZone, to: DragZone) { + fromDragZone = from + toDragZone = to + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt index 33f14acd0f02..391d46287498 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -157,33 +157,33 @@ class DesktopModeStatusTest : ShellTestCase() { } @Test - fun isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { - doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + fun isInternalDisplayEligibleToHostDesktops_configDEModeOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() { + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() { + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOn_configDevOptModeOn_returnsTrue() { + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_configDevOptModeOn_returnsTrue() { doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() } @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index b9d6a454694d..e5a6a6d258dd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -360,7 +360,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(commitWCT), commitWCT, mock(SurfaceControl.Transaction.class)); } assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -430,7 +431,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(restoreWCT), restoreWCT, mock(SurfaceControl.Transaction.class)); } assertTrue(mStageCoordinator.isSplitScreenVisible()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt index 53ae967e7bbf..067dcec5d65d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt @@ -73,7 +73,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : .spyStatic(DesktopModeStatus::class.java) .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(false).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(false).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())} setUpCommon() whenever(mockDisplayController.getDisplay(anyInt())).thenReturn(mockDisplay) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index f15418adf1e3..49812d381178 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -116,7 +116,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) } doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) } setUpCommon() @@ -384,7 +385,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) } setUpMockDecorationsForTasks(task) onTaskOpening(task) diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index b1550b0b6888..63a024b8e780 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -260,7 +260,7 @@ sk_sp<Bitmap> Bitmap::createFrom(AHardwareBuffer* hardwareBuffer, const SkImageI #endif sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly) { + size_t size, bool readOnly, int64_t id) { #ifdef _WIN32 // ashmem not implemented on Windows return nullptr; #else @@ -279,7 +279,7 @@ sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int f } } - sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes)); + sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes, id)); if (readOnly) { bitmap->setImmutable(); } @@ -334,7 +334,7 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) , mPixelStorageType(PixelStorageType::Ashmem) - , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) { + , mId(id != UNDEFINED_BITMAP_ID ? id : getId(mPixelStorageType)) { mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 8abe6a8c445a..4e9bcf27c0ef 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -97,7 +97,7 @@ public: BitmapPalette palette); #endif static sk_sp<Bitmap> createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly); + size_t size, bool readOnly, int64_t id); static sk_sp<Bitmap> createFrom(const SkImageInfo&, SkPixelRef&); int rowBytesAsPixels() const { return rowBytes() >> mInfo.shiftPerPixel(); } @@ -183,15 +183,15 @@ public: static bool compress(const SkBitmap& bitmap, JavaCompressFormat format, int32_t quality, SkWStream* stream); -private: - static constexpr uint64_t INVALID_BITMAP_ID = 0u; + static constexpr uint64_t UNDEFINED_BITMAP_ID = 0u; +private: static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes); Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info); Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes, - uint64_t id = INVALID_BITMAP_ID); + uint64_t id = UNDEFINED_BITMAP_ID); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes, BitmapPalette palette); diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 29efd98b41d0..cfde0b28c0d5 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -191,9 +191,8 @@ void reinitBitmap(JNIEnv* env, jobject javaBitmap, const SkImageInfo& info, info.width(), info.height(), isPremultiplied); } -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, - int density) { +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, + jobject ninePatchInsets, int density, int64_t id) { static jmethodID gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); @@ -208,10 +207,12 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, if (!isMutable) { bitmapWrapper->bitmap().setImmutable(); } + int64_t bitmapId = id != Bitmap::UNDEFINED_BITMAP_ID ? id : bitmap->getId(); jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, - static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper), - bitmap->width(), bitmap->height(), density, - isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc); + static_cast<jlong>(bitmapId), + reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), + bitmap->height(), density, isPremultiplied, ninePatchChunk, + ninePatchInsets, fromMalloc); if (env->ExceptionCheck() != 0) { ALOGE("*** Uncaught exception returned from Java call!\n"); @@ -759,6 +760,7 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { const int32_t height = p.readInt32(); const int32_t rowBytes = p.readInt32(); const int32_t density = p.readInt32(); + const int64_t sourceId = p.readInt64(); if (kN32_SkColorType != colorType && kRGBA_F16_SkColorType != colorType && @@ -815,7 +817,8 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { return STATUS_NO_MEMORY; } nativeBitmap = - Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable); + Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, + !isMutable, sourceId); return STATUS_OK; }); @@ -831,15 +834,15 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { } return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr, - nullptr, density); + nullptr, density, sourceId); #else jniThrowRuntimeException(env, "Cannot use parcels outside of Android"); return NULL; #endif } -static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, - jlong bitmapHandle, jint density, jobject parcel) { +static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density, + jobject parcel) { #ifdef __ANDROID__ // Layoutlib does not support parcel if (parcel == NULL) { ALOGD("------- writeToParcel null parcel\n"); @@ -870,6 +873,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, binder_status_t status; int fd = bitmapWrapper->bitmap().getAshmemFd(); if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) { + p.writeInt64(bitmapWrapper->bitmap().getId()); #if DEBUG_PARCEL ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as " "immutable blob (fds %s)", @@ -889,7 +893,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)", p.allowFds() ? "allowed" : "forbidden"); #endif - + p.writeInt64(Bitmap::UNDEFINED_BITMAP_ID); status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap); if (status) { doThrowRE(env, "Could not copy bitmap to parcel blob."); diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h index 21a93f066d9b..c93246a972b6 100644 --- a/libs/hwui/jni/Bitmap.h +++ b/libs/hwui/jni/Bitmap.h @@ -18,6 +18,7 @@ #include <jni.h> #include <android/bitmap.h> +#include <hwui/Bitmap.h> struct SkImageInfo; @@ -33,9 +34,9 @@ enum BitmapCreateFlags { kBitmapCreateFlag_Premultiplied = 0x2, }; -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk = nullptr, - jobject ninePatchInsets = nullptr, int density = -1); +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, + jbyteArray ninePatchChunk = nullptr, jobject ninePatchInsets = nullptr, + int density = -1, int64_t id = Bitmap::UNDEFINED_BITMAP_ID); Bitmap& toBitmap(jlong bitmapHandle); diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp index b0f5423813b7..95e4e01d8df8 100644 --- a/libs/hwui/jni/ScopedParcel.cpp +++ b/libs/hwui/jni/ScopedParcel.cpp @@ -39,6 +39,16 @@ uint32_t ScopedParcel::readUint32() { return temp; } +int64_t ScopedParcel::readInt64() { + int64_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readInt64(mParcel, &temp) != STATUS_OK) { + temp = 0; + } + return temp; +} + float ScopedParcel::readFloat() { float temp = 0.; if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) { diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h index fd8d6a210f0f..f2f138fda43c 100644 --- a/libs/hwui/jni/ScopedParcel.h +++ b/libs/hwui/jni/ScopedParcel.h @@ -35,12 +35,16 @@ public: uint32_t readUint32(); + int64_t readInt64(); + float readFloat(); void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); } void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); } + void writeInt64(int64_t value) { AParcel_writeInt64(mParcel, value); } + void writeFloat(float value) { AParcel_writeFloat(mParcel, value); } bool allowFds() const { return AParcel_getAllowFds(mParcel); } diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp index 747eb8e5ad1b..5406de8602d6 100644 --- a/libs/input/PointerControllerContext.cpp +++ b/libs/input/PointerControllerContext.cpp @@ -15,6 +15,7 @@ */ #include "PointerControllerContext.h" + #include "PointerController.h" namespace { @@ -184,7 +185,7 @@ void PointerControllerContext::PointerAnimator::handleVsyncEvents() { DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE]; while ((n = mDisplayEventReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) { for (size_t i = 0; i < static_cast<size_t>(n); ++i) { - if (buf[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) { + if (buf[i].header.type == DisplayEventType::DISPLAY_EVENT_VSYNC) { timestamp = buf[i].header.timestamp; gotVsync = true; } diff --git a/media/java/android/media/audiofx/HapticGenerator.java b/media/java/android/media/audiofx/HapticGenerator.java index d2523ef43b9e..7f94ddea9b84 100644 --- a/media/java/android/media/audiofx/HapticGenerator.java +++ b/media/java/android/media/audiofx/HapticGenerator.java @@ -36,6 +36,20 @@ import java.util.UUID; * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling audio * effects. + * + * <pre>{@code + * AudioManager audioManager = context.getSystemService(AudioManager.class); + * player = MediaPlayer.create( + * context, + * audioUri, + * new AudioAttributes.Builder().setHapticChannelsMuted(false).build(), + * audioManager.generateAudioSessionId() + * ); + * if (HapticGenerator.isAvailable()) { + * HapticGenerator.create(player.getAudioSessionId()).setEnabled(true); + * } + * player.start(); + * }</pre> */ public class HapticGenerator extends AudioEffect implements AutoCloseable { diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml index 96e5892f4d1d..bcc10ddde228 100644 --- a/packages/EasterEgg/AndroidManifest.xml +++ b/packages/EasterEgg/AndroidManifest.xml @@ -64,7 +64,7 @@ android:label="@string/u_egg_name" android:icon="@drawable/android16_patch_adaptive" android:configChanges="orientation|screenLayout|screenSize|density" - android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> + android:theme="@style/Theme.Landroid"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> diff --git a/packages/EasterEgg/res/drawable/ic_planet_large.xml b/packages/EasterEgg/res/drawable/ic_planet_large.xml new file mode 100644 index 000000000000..7ac7c38153f2 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_large.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_medium.xml b/packages/EasterEgg/res/drawable/ic_planet_medium.xml new file mode 100644 index 000000000000..e997b45eb6e5 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_medium.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_small.xml b/packages/EasterEgg/res/drawable/ic_planet_small.xml new file mode 100644 index 000000000000..43339573207b --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_small.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_tiny.xml b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml new file mode 100644 index 000000000000..c666765113da --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft.xml b/packages/EasterEgg/res/drawable/ic_spacecraft.xml new file mode 100644 index 000000000000..3cef4ab29192 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportHeight="24" android:viewportWidth="24" + > + <group android:translateX="10" android:translateY="12"> + <path + android:strokeColor="#FFFFFF" + android:strokeWidth="2" + android:pathData=" +M11.853 0 +C11.853 -4.418 8.374 -8 4.083 -8 +L-5.5 -8 +C-6.328 -8 -7 -7.328 -7 -6.5 +C-7 -5.672 -6.328 -5 -5.5 -5 +L-2.917 -5 +C-1.26 -5 0.083 -3.657 0.083 -2 +L0.083 2 +C0.083 3.657 -1.26 5 -2.917 5 +L-5.5 5 +C-6.328 5 -7 5.672 -7 6.5 +C-7 7.328 -6.328 8 -5.5 8 +L4.083 8 +C8.374 8 11.853 4.418 11.853 0 +Z + "/> + </group> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml new file mode 100644 index 000000000000..7a0c70379f20 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportHeight="24" android:viewportWidth="24" + > + <group android:translateX="10" android:translateY="12"> + <path + android:strokeColor="#FFFFFF" + android:fillColor="#000000" + android:strokeWidth="2" + android:pathData=" +M11.853 0 +C11.853 -4.418 8.374 -8 4.083 -8 +L-5.5 -8 +C-6.328 -8 -7 -7.328 -7 -6.5 +C-7 -5.672 -6.328 -5 -5.5 -5 +L-2.917 -5 +C-1.26 -5 0.083 -3.657 0.083 -2 +L0.083 2 +C0.083 3.657 -1.26 5 -2.917 5 +L-5.5 5 +C-6.328 5 -7 5.672 -7 6.5 +C-7 7.328 -6.328 8 -5.5 8 +L4.083 8 +C8.374 8 11.853 4.418 11.853 0 +Z + "/> + </group> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml new file mode 100644 index 000000000000..2d4ce106ef38 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 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. +--> +<rotate xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/ic_spacecraft" + android:fromDegrees="0" + android:toDegrees="360" + />
\ No newline at end of file diff --git a/packages/EasterEgg/res/values/themes.xml b/packages/EasterEgg/res/values/themes.xml index 5b163043a356..3a87e456fc3b 100644 --- a/packages/EasterEgg/res/values/themes.xml +++ b/packages/EasterEgg/res/values/themes.xml @@ -1,7 +1,26 @@ -<resources> +<?xml version="1.0" encoding="utf-8"?><!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> <style name="ThemeOverlay.EasterEgg.AppWidgetContainer" parent=""> <item name="appWidgetBackgroundColor">@color/light_blue_600</item> <item name="appWidgetTextColor">@color/light_blue_50</item> </style> -</resources>
\ No newline at end of file + + <style name="Theme.Landroid" parent="android:Theme.Material.NoActionBar"> + <item name="android:windowLightStatusBar">false</item> + <item name="android:windowLightNavigationBar">false</item> + </style> +</resources> diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt index fb5954ec9736..8214c540304e 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt @@ -41,14 +41,16 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity { val telemetry: String get() = - listOf( - "---- AUTOPILOT ENGAGED ----", - "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."), - "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "", - ) - .joinToString("\n") - - private var strategy: String = "NONE" + if (enabled) + listOf( + "---- AUTOPILOT ENGAGED ----", + "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."), + "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "", + ) + .joinToString("\n") + else "" + + var strategy: String = "NONE" private var debug: String = "" override fun update(sim: Simulator, dt: Float) { @@ -119,7 +121,7 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity { target.pos + Vec2.makeWithAngleMag( target.velocity.angle(), - min(altitude / 2, target.velocity.mag()) + min(altitude / 2, target.velocity.mag()), ) leadingVector = leadingPos - ship.pos diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt index d040fba49fdf..e74863849efa 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt @@ -20,9 +20,19 @@ import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.Easing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlin.random.Random @Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() } @@ -36,6 +46,40 @@ val flickerFadeIn = animationSpec = tween( durationMillis = 1000, - easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random) + easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random), ) ) + +fun flickerFadeInAfterDelay(delay: Int = 0) = + fadeIn( + animationSpec = + tween( + durationMillis = 1000, + delayMillis = delay, + easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random), + ) + ) + +@Composable +fun ConsoleButton( + modifier: Modifier = Modifier, + textStyle: TextStyle = TextStyle.Default, + color: Color, + bgColor: Color, + borderColor: Color, + text: String, + onClick: () -> Unit, +) { + Text( + style = textStyle, + color = color, + modifier = + modifier + .clickable { onClick() } + .background(color = bgColor) + .border(width = 1.dp, color = borderColor) + .padding(6.dp) + .minimumInteractiveComponentSize(), + text = text, + ) +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt index d56e8b9e8d0e..8d4adf638bb3 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt @@ -56,6 +56,8 @@ class DreamUniverse : DreamService() { } } + private var notifier: UniverseProgressNotifier? = null + override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -76,8 +78,8 @@ class DreamUniverse : DreamService() { Random.nextFloat() * PI2f, Random.nextFloatInRange( PLANET_ORBIT_RANGE.start, - PLANET_ORBIT_RANGE.endInclusive - ) + PLANET_ORBIT_RANGE.endInclusive, + ), ) } @@ -94,9 +96,11 @@ class DreamUniverse : DreamService() { composeView.setContent { Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState) DebugText(DEBUG_TEXT) - Telemetry(universe) + Telemetry(universe, showControls = false) } + notifier = UniverseProgressNotifier(this, universe) + composeView.setViewTreeLifecycleOwner(lifecycleOwner) composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner) diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt index 4f77b00b7570..95a60c7a5292 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt @@ -21,6 +21,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility @@ -34,6 +35,7 @@ import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -46,6 +48,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -59,6 +62,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.TextStyle @@ -74,9 +78,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.window.layout.FoldingFeature import androidx.window.layout.WindowInfoTracker -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import java.lang.Float.max import java.lang.Float.min import java.util.Calendar @@ -85,11 +86,14 @@ import kotlin.math.absoluteValue import kotlin.math.floor import kotlin.math.sqrt import kotlin.random.Random +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch enum class RandomSeedType { Fixed, Daily, - Evergreen + Evergreen, } const val TEST_UNIVERSE = false @@ -138,6 +142,10 @@ fun getDessertCode(): String = else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "") } +fun getSystemDesignation(universe: Universe): String { + return "${getDessertCode()}-${universe.randomSeed % 100_000}" +} + val DEBUG_TEXT = mutableStateOf("Hello Universe") const val SHOW_DEBUG_TEXT = false @@ -150,13 +158,13 @@ fun DebugText(text: MutableState<String>) { fontWeight = FontWeight.Medium, fontSize = 9.sp, color = Color.Yellow, - text = text.value + text = text.value, ) } } @Composable -fun Telemetry(universe: Universe) { +fun Telemetry(universe: Universe, showControls: Boolean) { var topVisible by remember { mutableStateOf(false) } var bottomVisible by remember { mutableStateOf(false) } @@ -174,7 +182,6 @@ fun Telemetry(universe: Universe) { LaunchedEffect("blah") { delay(1000) bottomVisible = true - delay(1000) topVisible = true } @@ -183,13 +190,11 @@ fun Telemetry(universe: Universe) { // TODO: Narrow the scope of invalidation here to the specific data needed; // the behavior below mimics the previous implementation of a snapshot ticker value val recomposeScope = currentRecomposeScope - Telescope(universe) { - recomposeScope.invalidate() - } + Telescope(universe) { recomposeScope.invalidate() } BoxWithConstraints( modifier = - Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent), + Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent) ) { val wide = maxWidth > maxHeight Column( @@ -197,57 +202,82 @@ fun Telemetry(universe: Universe) { Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart) .fillMaxWidth(if (wide) 0.45f else 1.0f) ) { - universe.ship.autopilot?.let { autopilot -> - if (autopilot.enabled) { + val autopilotEnabled = universe.ship.autopilot?.enabled == true + if (autopilotEnabled) { + universe.ship.autopilot?.let { autopilot -> AnimatedVisibility( modifier = Modifier, visible = bottomVisible, - enter = flickerFadeIn + enter = flickerFadeIn, ) { Text( style = textStyle, color = Colors.Autopilot, modifier = Modifier.align(Left), - text = autopilot.telemetry + text = autopilot.telemetry, ) } } } - AnimatedVisibility( - modifier = Modifier, - visible = bottomVisible, - enter = flickerFadeIn - ) { - Text( - style = textStyle, - color = Colors.Console, - modifier = Modifier.align(Left), - text = - with(universe.ship) { - val closest = universe.closestPlanet() - val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt() - listOfNotNull( - landing?.let { - "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}" - } - ?: if (distToClosest < 10_000) { - "ALT: $distToClosest" - } else null, - "THR: %.0f%%".format(thrust.mag() * 100f), - "POS: %s".format(pos.str("%+7.0f")), - "VEL: %.0f".format(velocity.mag()) - ) - .joinToString("\n") + Row(modifier = Modifier.padding(top = 6.dp)) { + AnimatedVisibility( + modifier = Modifier.weight(1f), + visible = bottomVisible, + enter = flickerFadeIn, + ) { + Text( + style = textStyle, + color = Colors.Console, + text = + with(universe.ship) { + val closest = universe.closestPlanet() + val distToClosest = + ((closest.pos - pos).mag() - closest.radius).toInt() + listOfNotNull( + landing?.let { + "LND: ${it.planet.name.toUpperCase()}\n" + + "JOB: ${it.text.toUpperCase()}" + } + ?: if (distToClosest < 10_000) { + "ALT: $distToClosest" + } else null, + "THR: %.0f%%".format(thrust.mag() * 100f), + "POS: %s".format(pos.str("%+7.0f")), + "VEL: %.0f".format(velocity.mag()), + ) + .joinToString("\n") + }, + ) + } + + if (showControls) { + AnimatedVisibility( + visible = bottomVisible, + enter = flickerFadeInAfterDelay(500), + ) { + ConsoleButton( + textStyle = textStyle, + color = Colors.Console, + bgColor = if (autopilotEnabled) Colors.Autopilot else Color.Transparent, + borderColor = Colors.Console, + text = "AUTO", + ) { + universe.ship.autopilot?.let { + it.enabled = !it.enabled + DYNAMIC_ZOOM = it.enabled + if (!it.enabled) universe.ship.thrust = Vec2.Zero + } } - ) + } + } } } AnimatedVisibility( modifier = Modifier.align(Alignment.TopStart), visible = topVisible, - enter = flickerFadeIn + enter = flickerFadeInAfterDelay(1000), ) { Text( style = textStyle, @@ -263,13 +293,12 @@ fun Telemetry(universe: Universe) { text = (with(universe.star) { listOf( - " STAR: $name (${getDessertCode()}-" + - "${universe.randomSeed % 100_000})", + " STAR: $name (${getSystemDesignation(universe)})", " CLASS: ${cls.name}", "RADIUS: ${radius.toInt()}", " MASS: %.3g".format(mass), "BODIES: ${explored.size} / ${universe.planets.size}", - "" + "", ) } + explored @@ -280,11 +309,11 @@ fun Telemetry(universe: Universe) { " ATMO: ${it.atmosphere.capitalize()}", " FAUNA: ${it.fauna.capitalize()}", " FLORA: ${it.flora.capitalize()}", - "" + "", ) } .flatten()) - .joinToString("\n") + .joinToString("\n"), // TODO: different colors, highlight latest discovery ) @@ -293,6 +322,7 @@ fun Telemetry(universe: Universe) { } class MainActivity : ComponentActivity() { + private var notifier: UniverseProgressNotifier? = null private var foldState = mutableStateOf<FoldingFeature?>(null) override fun onCreate(savedInstanceState: Bundle?) { @@ -300,7 +330,7 @@ class MainActivity : ComponentActivity() { onWindowLayoutInfoChange() - enableEdgeToEdge() + enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Red.toArgb())) val universe = Universe(namer = Namer(resources), randomSeed = randomSeed()) @@ -312,12 +342,13 @@ class MainActivity : ComponentActivity() { com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext) - // for autopilot testing in the activity - // val autopilot = Autopilot(universe.ship, universe) - // universe.ship.autopilot = autopilot - // universe.add(autopilot) - // autopilot.enabled = true - // DYNAMIC_ZOOM = autopilot.enabled + // set up the autopilot in case we need it + val autopilot = Autopilot(universe.ship, universe) + universe.ship.autopilot = autopilot + universe.add(autopilot) + autopilot.enabled = false + + notifier = UniverseProgressNotifier(this, universe) setContent { Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState) @@ -329,7 +360,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), minRadius = minRadius, maxRadius = maxRadius, - color = Color.Green + color = Color.Green, ) { vec -> (universe.follow as? Spacecraft)?.let { ship -> if (vec == Vec2.Zero) { @@ -346,13 +377,13 @@ class MainActivity : ComponentActivity() { ship.thrust = Vec2.makeWithAngleMag( a, - lexp(minRadius, maxRadius, m).coerceIn(0f, 1f) + lexp(minRadius, maxRadius, m).coerceIn(0f, 1f), ) } } } } - Telemetry(universe) + Telemetry(universe, true) } } @@ -382,7 +413,7 @@ fun MainActivityPreview() { Spaaaace(modifier = Modifier.fillMaxSize(), universe) DebugText(DEBUG_TEXT) - Telemetry(universe) + Telemetry(universe, true) } @Composable @@ -391,7 +422,7 @@ fun FlightStick( minRadius: Float = 0f, maxRadius: Float = 1000f, color: Color = Color.Green, - onStickChanged: (vector: Vec2) -> Unit + onStickChanged: (vector: Vec2) -> Unit, ) { val origin = remember { mutableStateOf(Vec2.Zero) } val target = remember { mutableStateOf(Vec2.Zero) } @@ -444,14 +475,14 @@ fun FlightStick( PathEffect.dashPathEffect( floatArrayOf(this.density * 1f, this.density * 2f) ) - else null - ) + else null, + ), ) drawLine( color = color, start = origin.value, end = origin.value + Vec2.makeWithAngleMag(a, mag), - strokeWidth = 2f + strokeWidth = 2f, ) } } @@ -462,15 +493,13 @@ fun FlightStick( fun Spaaaace( modifier: Modifier, u: Universe, - foldState: MutableState<FoldingFeature?> = mutableStateOf(null) + foldState: MutableState<FoldingFeature?> = mutableStateOf(null), ) { LaunchedEffect(u) { - while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> - u.step(frameTimeNanos) - } + while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> u.step(frameTimeNanos) } } - var cameraZoom by remember { mutableStateOf(1f) } + var cameraZoom by remember { mutableFloatStateOf(DEFAULT_CAMERA_ZOOM) } var cameraOffset by remember { mutableStateOf(Offset.Zero) } val transformableState = @@ -501,15 +530,16 @@ fun Spaaaace( val closest = u.closestPlanet() val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f) // val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f - if (DYNAMIC_ZOOM) { - cameraZoom = - expSmooth( - cameraZoom, - clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM), - dt = u.dt, - speed = 1.5f - ) - } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM + val targetZoom = + if (DYNAMIC_ZOOM) { + clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM) + } else { + DEFAULT_CAMERA_ZOOM + } + if (!TOUCH_CAMERA_ZOOM) { + cameraZoom = expSmooth(cameraZoom, targetZoom, dt = u.dt, speed = 1.5f) + } + if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f // cameraZoom: metersToPixels @@ -521,9 +551,9 @@ fun Spaaaace( -cameraOffset - Offset( visibleSpaceSizeMeters.width * centerFracX, - visibleSpaceSizeMeters.height * centerFracY + visibleSpaceSizeMeters.height * centerFracY, ), - visibleSpaceSizeMeters + visibleSpaceSizeMeters, ) var gridStep = 1000f @@ -537,14 +567,14 @@ fun Spaaaace( "fps: ${"%3.0f".format(1f / u.dt)} " + "dt: ${u.dt}\n" + ((u.follow as? Spacecraft)?.let { - "ship: p=%s v=%7.2f a=%6.3f t=%s\n".format( - it.pos.str("%+7.1f"), - it.velocity.mag(), - it.angle, - it.thrust.str("%+5.2f") - ) - } - ?: "") + + "ship: p=%s v=%7.2f a=%6.3f t=%s\n" + .format( + it.pos.str("%+7.1f"), + it.velocity.mag(), + it.angle, + it.thrust.str("%+5.2f"), + ) + } ?: "") + "star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " + "class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" + "planets: ${u.planets.size}\n" + @@ -574,7 +604,7 @@ fun Spaaaace( translate( -visibleSpaceRectMeters.center.x + size.width * 0.5f, - -visibleSpaceRectMeters.center.y + size.height * 0.5f + -visibleSpaceRectMeters.center.y + size.height * 0.5f, ) { // debug outer frame // drawRect( @@ -590,7 +620,7 @@ fun Spaaaace( color = Colors.Eigengrau2, start = Offset(x, visibleSpaceRectMeters.top), end = Offset(x, visibleSpaceRectMeters.bottom), - strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom, ) x += gridStep } @@ -601,7 +631,7 @@ fun Spaaaace( color = Colors.Eigengrau2, start = Offset(visibleSpaceRectMeters.left, y), end = Offset(visibleSpaceRectMeters.right, y), - strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom, ) y += gridStep } diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt index 73318077f47a..babf1328c7d4 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt @@ -16,8 +16,8 @@ package com.android.egg.landroid -import android.content.res.Resources import com.android.egg.R +import android.content.res.Resources import kotlin.random.Random const val SUFFIX_PROB = 0.75f @@ -58,7 +58,7 @@ class Namer(resources: Resources) { 1f to "*", 1f to "^", 1f to "#", - 0.1f to "(^*!%@##!!" + 0.1f to "(^*!%@##!!", ) private var activities = Bag(resources.getStringArray(R.array.activities)) @@ -101,26 +101,26 @@ class Namer(resources: Resources) { fun floraPlural(rng: Random): String { return floraGenericPlurals.pull(rng) } + fun faunaPlural(rng: Random): String { return faunaGenericPlurals.pull(rng) } + fun atmoPlural(rng: Random): String { return atmoGenericPlurals.pull(rng) } val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""") + fun describeActivity(rng: Random, target: Planet?): String { - return activities - .pull(rng) - .replace(TEMPLATE_REGEX) { - when (it.groupValues[1]) { - "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng) - "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng) - "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng) - "planet" -> (target?.description ?: "SOME BODY") // once told me - else -> "unknown template tag: ${it.groupValues[0]}" - } + return activities.pull(rng).replace(TEMPLATE_REGEX) { + when (it.groupValues[1]) { + "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng) + "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng) + "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng) + "planet" -> (target?.description ?: "SOME BODY") // once told me + else -> "unknown template tag: ${it.groupValues[0]}" } - .toUpperCase() + } } } diff --git a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt new file mode 100644 index 000000000000..bb3a04df6f36 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2025 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.egg.landroid + +import com.android.egg.R + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.DisposableHandle + +const val CHANNEL_ID = "progress" +const val CHANNEL_NAME = "Spacecraft progress" +const val UPDATE_FREQUENCY_SEC = 1f + +fun lerpRange(range: ClosedFloatingPointRange<Float>, x: Float): Float = + lerp(range.start, range.endInclusive, x) + +class UniverseProgressNotifier(val context: Context, val universe: Universe) { + private val notificationId = universe.randomSeed.toInt() + private val chan = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + .apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC } + private val noman = + context.getSystemService(NotificationManager::class.java)?.apply { + createNotificationChannel(chan) + } + + private val registration: DisposableHandle = + universe.addSimulationStepListener(this::onSimulationStep) + + private val spacecraftIcon = Icon.createWithResource(context, R.drawable.ic_spacecraft_filled) + private val planetIcons = + listOf( + (lerpRange(PLANET_RADIUS_RANGE, 0.75f)) to + Icon.createWithResource(context, R.drawable.ic_planet_large), + (lerpRange(PLANET_RADIUS_RANGE, 0.5f)) to + Icon.createWithResource(context, R.drawable.ic_planet_medium), + (lerpRange(PLANET_RADIUS_RANGE, 0.25f)) to + Icon.createWithResource(context, R.drawable.ic_planet_small), + (PLANET_RADIUS_RANGE.start to + Icon.createWithResource(context, R.drawable.ic_planet_tiny)), + ) + + private fun getPlanetIcon(planet: Planet): Icon { + for ((radius, icon) in planetIcons) { + if (planet.radius > radius) return icon + } + return planetIcons.last().second + } + + private val progress = Notification.ProgressStyle().setProgressTrackerIcon(spacecraftIcon) + + private val builder = + Notification.Builder(context, CHANNEL_ID) + .setContentIntent( + PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + ) + .setPriority(Notification.PRIORITY_DEFAULT) + .setColorized(true) + .setOngoing(true) + .setColor(Colors.Eigengrau2.toArgb()) + .setStyle(progress) + + private var lastUpdate = 0f + private var initialDistToTarget = 0 + + private fun onSimulationStep() { + if (universe.now - lastUpdate >= UPDATE_FREQUENCY_SEC) { + lastUpdate = universe.now + // android.util.Log.v("Landroid", "posting notification at time ${universe.now}") + + var distToTarget = 0 + val autopilot = universe.ship.autopilot + val autopilotEnabled: Boolean = autopilot?.enabled == true + val target = autopilot?.target + val landing = universe.ship.landing + val speed = universe.ship.velocity.mag() + + if (landing != null) { + // landed + builder.setContentTitle("landed: ${landing.planet.name}") + builder.setContentText("currently: ${landing.text}") + builder.setShortCriticalText("landed") + + progress.setProgress(progress.progressMax) + progress.setProgressIndeterminate(false) + + builder.setStyle(progress) + } else if (autopilotEnabled) { + if (target != null) { + // autopilot en route + distToTarget = ((target.pos - universe.ship.pos).mag() - target.radius).toInt() + if (initialDistToTarget == 0) { + // we have a new target! + initialDistToTarget = distToTarget + progress.progressEndIcon = getPlanetIcon(target) + } + + val eta = if (speed > 0) "%1.0fs".format(distToTarget / speed) else "???" + builder.setContentTitle("headed to: ${target.name}") + builder.setContentText( + "autopilot is ${autopilot.strategy.toLowerCase()}" + + "\ndist: ${distToTarget}u // eta: $eta" + ) + // fun fact: ProgressStyle was originally EnRouteStyle + builder.setShortCriticalText("en route") + + progress + .setProgressSegments( + listOf( + Notification.ProgressStyle.Segment(initialDistToTarget) + .setColor(Colors.Track.toArgb()) + ) + ) + .setProgress(initialDistToTarget - distToTarget) + .setProgressIndeterminate(false) + builder.setStyle(progress) + } else { + // no target + if (initialDistToTarget != 0) { + // just launched + initialDistToTarget = 0 + progress.progressStartIcon = progress.progressEndIcon + progress.progressEndIcon = null + } + + builder.setContentTitle("in space") + builder.setContentText("selecting new target...") + builder.setShortCriticalText("launched") + + progress.setProgressIndeterminate(true) + + builder.setStyle(progress) + } + } else { + // under user control + + initialDistToTarget = 0 + + builder.setContentTitle("in space") + builder.setContentText("under manual control") + builder.setShortCriticalText("adrift") + + builder.setStyle(null) + } + + builder + .setSubText(getSystemDesignation(universe)) + .setSmallIcon(R.drawable.ic_spacecraft_rotated) + + val notification = builder.build() + + // one of the silliest things about Android is that icon levels go from 0 to 10000 + notification.iconLevel = (((universe.ship.angle + PI2f) / PI2f) * 10_000f).toInt() + + noman?.notify(notificationId, notification) + } + } +} diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index ec287c1b65b7..52a2160cdd74 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -95,6 +95,8 @@ message PreferenceProto { optional PermissionsProto write_permissions = 18; // Tag constants associated with the preference. repeated string tags = 19; + // Permit to read and write preference value (the lower 15 bits is reserved for read permit). + optional int32 read_write_permit = 20; // Target of an Intent message ActionTarget { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index e511bf1c175d..13541b1ebc9a 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -56,6 +56,8 @@ import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceTitleProvider import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY +import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY import com.android.settingslib.preference.PreferenceScreenFactory import com.android.settingslib.preference.PreferenceScreenProvider import java.util.Locale @@ -415,52 +417,46 @@ fun PreferenceMetadata.toProto( for (tag in metadata.tags(context)) addTags(tag) } persistent = metadata.isPersistent(context) - if (persistent) { - if (metadata is PersistentPreference<*>) { - sensitivityLevel = metadata.sensitivityLevel - metadata.getReadPermissions(context)?.let { - if (it.size > 0) readPermissions = it.toProto() - } - metadata.getWritePermissions(context)?.let { - if (it.size > 0) writePermissions = it.toProto() + if (metadata !is PersistentPreference<*>) return@preferenceProto + sensitivityLevel = metadata.sensitivityLevel + metadata.getReadPermissions(context)?.let { if (it.size > 0) readPermissions = it.toProto() } + metadata.getWritePermissions(context)?.let { if (it.size > 0) writePermissions = it.toProto() } + val readPermit = metadata.evalReadPermit(context, callingPid, callingUid) + val writePermit = + metadata.evalWritePermit(context, callingPid, callingUid) ?: ReadWritePermit.ALLOW + readWritePermit = ReadWritePermit.make(readPermit, writePermit) + if ( + flags.includeValue() && + enabled && + (!hasAvailable() || available) && + (!hasRestricted() || !restricted) && + readPermit == ReadWritePermit.ALLOW + ) { + val storage = metadata.storage(context) + value = preferenceValueProto { + when (metadata.valueType) { + Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it } + Boolean::class.javaObjectType -> + storage.getBoolean(metadata.key)?.let { booleanValue = it } + Float::class.javaObjectType -> + storage.getFloat(metadata.key)?.let { floatValue = it } + else -> {} } } - if ( - flags.includeValue() && - enabled && - (!hasAvailable() || available) && - (!hasRestricted() || !restricted) && - metadata is PersistentPreference<*> && - metadata.evalReadPermit(context, callingPid, callingUid) == ReadWritePermit.ALLOW - ) { - val storage = metadata.storage(context) - value = preferenceValueProto { - when (metadata.valueType) { - Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it } - Boolean::class.javaObjectType -> - storage.getBoolean(metadata.key)?.let { booleanValue = it } - Float::class.javaObjectType -> - storage.getFloat(metadata.key)?.let { floatValue = it } - else -> {} - } - } - } - if (flags.includeValueDescriptor()) { - valueDescriptor = preferenceValueDescriptorProto { - when (metadata) { - is IntRangeValuePreference -> rangeValue = rangeValueProto { - min = metadata.getMinValue(context) - max = metadata.getMaxValue(context) - step = metadata.getIncrementStep(context) - } - else -> {} - } - if (metadata is PersistentPreference<*>) { - when (metadata.valueType) { - Boolean::class.javaObjectType -> booleanType = true - Float::class.javaObjectType -> floatType = true + } + if (flags.includeValueDescriptor()) { + valueDescriptor = preferenceValueDescriptorProto { + when (metadata) { + is IntRangeValuePreference -> rangeValue = rangeValueProto { + min = metadata.getMinValue(context) + max = metadata.getMaxValue(context) + step = metadata.getIncrementStep(context) } - } + else -> {} + } + when (metadata.valueType) { + Boolean::class.javaObjectType -> booleanType = true + Float::class.javaObjectType -> floatType = true } } } @@ -478,6 +474,20 @@ fun <T> PersistentPreference<T>.evalReadPermit( else -> getReadPermit(context, callingPid, callingUid) } +/** Evaluates the write permit of a persistent preference. */ +fun <T> PersistentPreference<T>.evalWritePermit( + context: Context, + callingPid: Int, + callingUid: Int, +): Int? = + when { + sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> + ReadWritePermit.DISALLOW + getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> + ReadWritePermit.REQUIRE_APP_PERMISSION + else -> getWritePermit(context, callingPid, callingUid) + } + private fun PreferenceMetadata.getTitleTextProto(context: Context, isRoot: Boolean): TextProto? { if (isRoot && this is PreferenceScreenMetadata) { val titleRes = screenTitle diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index 60f9c6bb92a3..72f6934b5f35 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -36,8 +36,6 @@ import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.ReadWritePermit -import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY -import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ class PreferenceSetterRequest( @@ -223,13 +221,8 @@ fun <T> PersistentPreference<T>.evalWritePermit( callingPid: Int, callingUid: Int, ): Int = - when { - sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> - ReadWritePermit.DISALLOW - getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> - ReadWritePermit.REQUIRE_APP_PERMISSION - else -> getWritePermit(context, value, callingPid, callingUid) - } + evalWritePermit(context, callingPid, callingUid) + ?: getWritePermit(context, value, callingPid, callingUid) /** Message codec for [PreferenceSetterRequest]. */ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> { diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index e456a7f1aa1c..c723dce82b5a 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -41,6 +41,19 @@ annotation class ReadWritePermit { const val REQUIRE_APP_PERMISSION = 2 /** Require explicit user agreement (e.g. terms of service). */ const val REQUIRE_USER_AGREEMENT = 3 + + private const val READ_PERMIT_BITS = 15 + private const val READ_PERMIT_MASK = (1 shl 16) - 1 + + /** Wraps given read and write permit into an integer. */ + fun make(readPermit: @ReadWritePermit Int, writePermit: @ReadWritePermit Int): Int = + (writePermit shl READ_PERMIT_BITS) or readPermit + + /** Extracts the read permit from given integer generated by [make]. */ + fun getReadPermit(readWritePermit: Int): Int = readWritePermit and READ_PERMIT_MASK + + /** Extracts the write permit from given integer generated by [make]. */ + fun getWritePermit(readWritePermit: Int): Int = readWritePermit shr READ_PERMIT_BITS } } @@ -81,6 +94,12 @@ interface PersistentPreference<T> : PreferenceMetadata { /** The value type the preference is associated with. */ val valueType: Class<T> + /** The sensitivity level of the preference. */ + val sensitivityLevel: @SensitivityLevel Int + get() = SensitivityLevel.UNKNOWN_SENSITIVITY + + override fun isPersistent(context: Context) = true + /** * Returns the key-value storage of the preference. * @@ -102,19 +121,27 @@ interface PersistentPreference<T> : PreferenceMetadata { * behind the scene. */ fun getReadPermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int = - PreferenceScreenRegistry.getReadPermit( - context, - callingPid, - callingUid, - this, - ) + PreferenceScreenRegistry.defaultReadPermit /** Returns the required permissions to write preference value. */ fun getWritePermissions(context: Context): Permissions? = null /** * Returns if the external application (identified by [callingPid] and [callingUid]) is - * permitted to write preference with given [value]. + * permitted to write preference value. If the write permit depends on certain value, implement + * the overloading [getWritePermit] instead. + * + * The underlying implementation does NOT need to check common states like isEnabled, + * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it + * behind the scene. + */ + fun getWritePermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int? = + null + + /** + * Returns if the external application (identified by [callingPid] and [callingUid]) is + * permitted to write preference with given [value]. Note that if the overloading + * [getWritePermit] returns non null value, this method will be ignored! * * The underlying implementation does NOT need to check common states like isEnabled, * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it @@ -125,18 +152,7 @@ interface PersistentPreference<T> : PreferenceMetadata { value: T?, callingPid: Int, callingUid: Int, - ): @ReadWritePermit Int = - PreferenceScreenRegistry.getWritePermit( - context, - value, - callingPid, - callingUid, - this, - ) - - /** The sensitivity level of the preference. */ - val sensitivityLevel: @SensitivityLevel Int - get() = SensitivityLevel.UNKNOWN_SENSITIVITY + ): @ReadWritePermit Int = PreferenceScreenRegistry.defaultWritePermit } /** Descriptor of values. */ diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt index a8939ab0d902..7f2a61081fbb 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt @@ -127,7 +127,7 @@ interface PreferenceMetadata { fun dependencies(context: Context): Array<String> = arrayOf() /** Returns if the preference is persistent in datastore. */ - fun isPersistent(context: Context): Boolean = this is PersistentPreference<*> + fun isPersistent(context: Context): Boolean = false /** * Returns if preference value backup is allowed (by default returns `true` if preference is diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt index 246310984db9..8d4bfffb1fdb 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt @@ -22,12 +22,18 @@ import android.util.Log import com.android.settingslib.datastore.KeyValueStore /** Registry of all available preference screens in the app. */ -object PreferenceScreenRegistry : ReadWritePermitProvider { +object PreferenceScreenRegistry { private const val TAG = "ScreenRegistry" /** Provider of key-value store. */ private lateinit var keyValueStoreProvider: KeyValueStoreProvider + /** The default permit for external application to read preference values. */ + var defaultReadPermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW + + /** The default permit for external application to write preference values. */ + var defaultWritePermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW + /** * Factories of all available [PreferenceScreenMetadata]s. * @@ -38,9 +44,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { /** Metrics logger for preference actions triggered by user interaction. */ var preferenceUiActionMetricsLogger: PreferenceUiActionMetricsLogger? = null - private var readWritePermitProvider: ReadWritePermitProvider = - object : ReadWritePermitProvider {} - /** Sets the [KeyValueStoreProvider]. */ fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) { this.keyValueStoreProvider = keyValueStoreProvider @@ -77,28 +80,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { return null } } - - /** - * Sets the provider to check read write permit. Read and write requests are denied by default. - */ - fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider) { - this.readWritePermitProvider = readWritePermitProvider - } - - override fun getReadPermit( - context: Context, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ) = readWritePermitProvider.getReadPermit(context, callingPid, callingUid, preference) - - override fun getWritePermit( - context: Context, - value: Any?, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ) = readWritePermitProvider.getWritePermit(context, value, callingPid, callingUid, preference) } /** Provider of [KeyValueStore]. */ @@ -113,25 +94,3 @@ fun interface KeyValueStoreProvider { */ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? } - -/** Provider of read and write permit. */ -interface ReadWritePermitProvider { - - val defaultReadWritePermit: @ReadWritePermit Int - get() = ReadWritePermit.DISALLOW - - fun getReadPermit( - context: Context, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ): @ReadWritePermit Int = defaultReadWritePermit - - fun getWritePermit( - context: Context, - value: Any?, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ): @ReadWritePermit Int = defaultReadWritePermit -} diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java index e173c5e996df..0f6a2a082e0c 100644 --- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java +++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java @@ -118,6 +118,7 @@ public class SettingsSpinnerPreference extends Preference spinner.setAdapter(mAdapter); spinner.setSelection(mPosition); spinner.setOnItemSelectedListener(mOnSelectedListener); + spinner.setLongClickable(false); if (mShouldPerformClick) { mShouldPerformClick = false; // To show dropdown view. diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index bf86911ee683..572444edea29 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -30,11 +30,13 @@ import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.flags.Flags; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -385,7 +387,7 @@ public class CsipDeviceManager { preferredMainDevice.refresh(); hasChanged = true; } - syncAudioSharingSourceIfNeeded(preferredMainDevice); + syncAudioSharingStatusIfNeeded(preferredMainDevice); } if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " @@ -399,13 +401,16 @@ public class CsipDeviceManager { return userManager != null && userManager.isManagedProfile(); } - private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) { + private void syncAudioSharingStatusIfNeeded(CachedBluetoothDevice mainDevice) { boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext); - if (isAudioSharingEnabled) { + if (isAudioSharingEnabled && mainDevice != null) { if (isWorkProfile()) { - log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, work profile"); return; } + Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); + deviceSet.add(mainDevice); + deviceSet.addAll(mainDevice.getMemberDevice()); boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager) && BluetoothUtils.hasConnectedBroadcastSource( mainDevice, mBtManager); @@ -419,9 +424,6 @@ public class CsipDeviceManager { if (metadata != null && assistant != null) { log("addMemberDevicesIntoMainDevice: sync audio sharing source after " + "combining the top level devices."); - Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); - deviceSet.add(mainDevice); - deviceSet.addAll(mainDevice.getMemberDevice()); Set<BluetoothDevice> sinksToSync = deviceSet.stream() .map(CachedBluetoothDevice::getDevice) .filter(device -> @@ -435,8 +437,24 @@ public class CsipDeviceManager { } } } + if (Flags.enableTemporaryBondDevicesUi()) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for audio sharing " + + "sinks after combining the top level devices."); + Set<BluetoothDevice> sinksToSync = deviceSet.stream() + .map(CachedBluetoothDevice::getDevice).filter(Objects::nonNull).collect( + Collectors.toSet()); + if (sinksToSync.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice)) { + for (BluetoothDevice device : sinksToSync) { + if (!BluetoothUtils.isTemporaryBondDevice(device)) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for " + + device.getAnonymizedAddress()); + BluetoothUtils.setTemporaryBondMetadata(device); + } + } + } + } } else { - log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, flag disabled"); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt index 10156c404ebf..bac564c7d0f4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt @@ -20,6 +20,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.media.MediaMetadata import android.media.session.MediaController +import android.media.session.MediaController.PlaybackInfo import android.media.session.MediaSession import android.media.session.MediaSessionManager import android.media.session.PlaybackState @@ -98,16 +99,22 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { } /** Set volume `level` to remote media `token` */ - fun setVolume(token: MediaSession.Token, level: Int) { + fun setVolume(sessionId: SessionId, volumeLevel: Int) { + when (sessionId) { + is SessionId.Media -> setMediaSessionVolume(sessionId.token, volumeLevel) + } + } + + private fun setMediaSessionVolume(token: MediaSession.Token, volumeLevel: Int) { val record = mRecords[token] if (record == null) { Log.w(TAG, "setVolume: No record found for token $token") return } if (D.BUG) { - Log.d(TAG, "Setting level to $level") + Log.d(TAG, "Setting level to $volumeLevel") } - record.controller.setVolumeTo(level, 0) + record.controller.setVolumeTo(volumeLevel, 0) } private fun onRemoteVolumeChangedH(sessionToken: MediaSession.Token, flags: Int) { @@ -122,7 +129,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { ) } val token = controller.sessionToken - mCallbacks.onRemoteVolumeChanged(token, flags) + mCallbacks.onRemoteVolumeChanged(SessionId.from(token), flags) } private fun onUpdateRemoteSessionListH(sessionToken: MediaSession.Token?) { @@ -158,7 +165,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { controller.registerCallback(record, mHandler) } val record = mRecords[token] - val remote = isRemote(playbackInfo) + val remote = playbackInfo.isRemote() if (remote) { updateRemoteH(token, record!!.name, playbackInfo) record.sentRemote = true @@ -172,7 +179,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { Log.d(TAG, "Removing " + record.name + " sentRemote=" + record.sentRemote) } if (record.sentRemote) { - mCallbacks.onRemoteRemoved(token) + mCallbacks.onRemoteRemoved(SessionId.from(token)) record.sentRemote = false } } @@ -213,8 +220,8 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { private fun updateRemoteH( token: MediaSession.Token, name: String?, - pi: MediaController.PlaybackInfo, - ) = mCallbacks.onRemoteUpdate(token, name, pi) + playbackInfo: PlaybackInfo, + ) = mCallbacks.onRemoteUpdate(SessionId.from(token), name, VolumeInfo.from(playbackInfo)) private inner class MediaControllerRecord(val controller: MediaController) : MediaController.Callback() { @@ -225,7 +232,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { return method + " " + controller.packageName + " " } - override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) { + override fun onAudioInfoChanged(info: PlaybackInfo) { if (D.BUG) { Log.d( TAG, @@ -235,9 +242,9 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { sentRemote), ) } - val remote = isRemote(info) + val remote = info.isRemote() if (!remote && sentRemote) { - mCallbacks.onRemoteRemoved(controller.sessionToken) + mCallbacks.onRemoteRemoved(SessionId.from(controller.sessionToken)) sentRemote = false } else if (remote) { updateRemoteH(controller.sessionToken, name, info) @@ -301,20 +308,36 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { } } + /** Opaque id for ongoing sessions that support volume adjustment. */ + sealed interface SessionId { + + companion object { + fun from(token: MediaSession.Token) = Media(token) + } + + data class Media(val token: MediaSession.Token) : SessionId + } + + /** Holds session volume information. */ + data class VolumeInfo(val currentVolume: Int, val maxVolume: Int) { + + companion object { + + fun from(playbackInfo: PlaybackInfo) = + VolumeInfo(playbackInfo.currentVolume, playbackInfo.maxVolume) + } + } + /** Callback for remote media sessions */ interface Callbacks { /** Invoked when remote media session is updated */ - fun onRemoteUpdate( - token: MediaSession.Token?, - name: String?, - pi: MediaController.PlaybackInfo?, - ) + fun onRemoteUpdate(token: SessionId?, name: String?, volumeInfo: VolumeInfo?) /** Invoked when remote media session is removed */ - fun onRemoteRemoved(token: MediaSession.Token?) + fun onRemoteRemoved(token: SessionId?) /** Invoked when remote volume is changed */ - fun onRemoteVolumeChanged(token: MediaSession.Token?, flags: Int) + fun onRemoteVolumeChanged(token: SessionId?, flags: Int) } companion object { @@ -325,12 +348,11 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { const val UPDATE_REMOTE_SESSION_LIST: Int = 3 private const val USE_SERVICE_LABEL = false - - private fun isRemote(pi: MediaController.PlaybackInfo?): Boolean = - pi != null && pi.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE } } +private fun PlaybackInfo?.isRemote() = this?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE + private fun MediaController.dump(n: Int, writer: PrintWriter) { writer.println(" Controller $n: $packageName") diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index fd14d1ff6786..2eccaa626f3b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -40,6 +40,8 @@ import android.content.Context; import android.os.Looper; import android.os.Parcel; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.settingslib.flags.Flags; @@ -74,6 +76,9 @@ public class CsipDeviceManagerTest { private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private static final String TEMP_BOND_METADATA = + "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; private final static int GROUP1 = 1; private final BluetoothClass DEVICE_CLASS_1 = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); @@ -337,6 +342,7 @@ public class CsipDeviceManagerTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list @@ -346,7 +352,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -359,6 +364,7 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() { // Condition: The preferredDevice is main and there is another main device in top list @@ -369,7 +375,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -377,6 +382,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); when(mUserManager.isManagedProfile()).thenReturn(true); @@ -387,10 +394,13 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncState() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice1; @@ -399,7 +409,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -407,6 +416,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -415,6 +426,8 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test @@ -436,13 +449,13 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(false); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) @@ -457,16 +470,20 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant, never()).addSource(any(BluetoothDevice.class), any(BluetoothLeBroadcastMetadata.class), anyBoolean()); + verify(mDevice2, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncState() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -474,6 +491,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state)); + when(mDevice1.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -488,6 +507,10 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false); verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false); + verify(mDevice2).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test diff --git a/packages/Shell/src/com/android/shell/BugreportPrefs.java b/packages/Shell/src/com/android/shell/BugreportPrefs.java index 93690d48cd04..b0fd925daec3 100644 --- a/packages/Shell/src/com/android/shell/BugreportPrefs.java +++ b/packages/Shell/src/com/android/shell/BugreportPrefs.java @@ -23,25 +23,24 @@ import android.content.SharedPreferences; * Preferences related to bug reports. */ final class BugreportPrefs { - static final String PREFS_BUGREPORT = "bugreports"; - - private static final String KEY_WARNING_STATE = "warning-state"; - - static final int STATE_UNKNOWN = 0; - // Shows the warning dialog. - static final int STATE_SHOW = 1; - // Skips the warning dialog. - static final int STATE_HIDE = 2; static int getWarningState(Context context, int def) { - final SharedPreferences prefs = context.getSharedPreferences( - PREFS_BUGREPORT, Context.MODE_PRIVATE); - return prefs.getInt(KEY_WARNING_STATE, def); + String prefsBugreport = context.getResources().getString( + com.android.internal.R.string.prefs_bugreport); + String keyWarningState = context.getResources().getString( + com.android.internal.R.string.key_warning_state); + final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport, + Context.MODE_PRIVATE); + return prefs.getInt(keyWarningState, def); } static void setWarningState(Context context, int value) { - final SharedPreferences prefs = context.getSharedPreferences( - PREFS_BUGREPORT, Context.MODE_PRIVATE); - prefs.edit().putInt(KEY_WARNING_STATE, value).apply(); + String prefsBugreport = context.getResources().getString( + com.android.internal.R.string.prefs_bugreport); + String keyWarningState = context.getResources().getString( + com.android.internal.R.string.key_warning_state); + final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport, + Context.MODE_PRIVATE); + prefs.edit().putInt(keyWarningState, value).apply(); } } diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index 61f49db07abc..fb0678fedb56 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -21,8 +21,6 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELEVISION; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.flags.Flags.handleBugreportsForWear; @@ -1347,7 +1345,11 @@ public class BugreportProgressService extends Service { } private boolean hasUserDecidedNotToGetWarningMessage() { - return getWarningState(mContext, STATE_UNKNOWN) == STATE_HIDE; + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + return getWarningState(mContext, bugreportStateUnknown) == bugreportStateHide; } private void maybeShowWarningMessageAndCloseNotification(int id) { diff --git a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java index a44e23603f52..0e835f91aca6 100644 --- a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java +++ b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java @@ -16,9 +16,6 @@ package com.android.shell; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_SHOW; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.BugreportPrefs.setWarningState; import static com.android.shell.BugreportProgressService.sendShareIntent; @@ -69,12 +66,19 @@ public class BugreportWarningActivity extends AlertActivity mConfirmRepeat = (CheckBox) ap.mView.findViewById(android.R.id.checkbox); - final int state = getWarningState(this, STATE_UNKNOWN); + int bugreportStateUnknown = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + int bugreportStateShow = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); + + final int state = getWarningState(this, bugreportStateUnknown); final boolean checked; if (Build.IS_USER) { - checked = state == STATE_HIDE; // Only checks if specifically set to. + checked = state == bugreportStateHide; // Only checks if specifically set to. } else { - checked = state != STATE_SHOW; // Checks by default. + checked = state != bugreportStateShow; // Checks by default. } mConfirmRepeat.setChecked(checked); @@ -83,9 +87,14 @@ public class BugreportWarningActivity extends AlertActivity @Override public void onClick(DialogInterface dialog, int which) { + int bugreportStateHide = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + int bugreportStateShow = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); if (which == AlertDialog.BUTTON_POSITIVE) { // Remember confirm state, and launch target - setWarningState(this, mConfirmRepeat.isChecked() ? STATE_HIDE : STATE_SHOW); + setWarningState(this, mConfirmRepeat.isChecked() ? bugreportStateHide + : bugreportStateShow); if (mSendIntent != null) { sendShareIntent(this, mSendIntent); } diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java index 7bda2ea790b0..2d6abe6cdc93 100644 --- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -19,10 +19,6 @@ package com.android.shell; import static android.test.MoreAsserts.assertContainsRegex; import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME; -import static com.android.shell.BugreportPrefs.PREFS_BUGREPORT; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_SHOW; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.BugreportPrefs.setWarningState; import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_REQUESTED; @@ -201,8 +197,9 @@ public class BugreportReceiverTest { return null; }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(), any(), anyBoolean(), anyBoolean()); - - setWarningState(mContext, STATE_HIDE); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + setWarningState(mContext, bugreportStateHide); mUiBot.turnScreenOn(); } @@ -469,22 +466,31 @@ public class BugreportReceiverTest { @Test public void testBugreportFinished_withWarningUnknownState() throws Exception { - bugreportFinishedWithWarningTest(STATE_UNKNOWN); + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + bugreportFinishedWithWarningTest(bugreportStateUnknown); } @Test public void testBugreportFinished_withWarningShowAgain() throws Exception { - bugreportFinishedWithWarningTest(STATE_SHOW); + int bugreportStateShow = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); + bugreportFinishedWithWarningTest(bugreportStateShow); } private void bugreportFinishedWithWarningTest(Integer propertyState) throws Exception { + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); if (propertyState == null) { // Clear properties - mContext.getSharedPreferences(PREFS_BUGREPORT, Context.MODE_PRIVATE) - .edit().clear().commit(); + mContext.getSharedPreferences( + mContext.getResources().getString(com.android.internal.R.string.prefs_bugreport) + , Context.MODE_PRIVATE).edit().clear().commit(); // Confidence check... - assertEquals("Did not reset properties", STATE_UNKNOWN, - getWarningState(mContext, STATE_UNKNOWN)); + assertEquals("Did not reset properties", bugreportStateUnknown, + getWarningState(mContext, bugreportStateUnknown)); } else { setWarningState(mContext, propertyState); } @@ -501,7 +507,8 @@ public class BugreportReceiverTest { // TODO: get ok and dontShowAgain from the dialog reference above UiObject dontShowAgain = mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_dont_repeat)); - final boolean firstTime = propertyState == null || propertyState == STATE_UNKNOWN; + final boolean firstTime = + propertyState == null || propertyState == bugreportStateUnknown; if (firstTime) { if (Build.IS_USER) { assertFalse("Checkbox should NOT be checked by default on user builds", @@ -524,8 +531,8 @@ public class BugreportReceiverTest { assertActionSendMultiple(extras); // Make sure it's hidden now. - int newState = getWarningState(mContext, STATE_UNKNOWN); - assertEquals("Didn't change state", STATE_HIDE, newState); + int newState = getWarningState(mContext, bugreportStateUnknown); + assertEquals("Didn't change state", bugreportStateHide, newState); } @Test diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 1a6365433be5..19806e7cdf64 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -207,6 +207,8 @@ filegroup { "tests/src/**/systemui/statusbar/notification/row/NotificationConversationInfoTest.java", "tests/src/**/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt", "tests/src/**/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt", + "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java", + "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierDisabledTest.java", "tests/src/**/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java", "tests/src/**/systemui/statusbar/phone/CentralSurfacesImplTest.java", "tests/src/**/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java", @@ -553,6 +555,11 @@ android_library { }, } +platform_compat_config { + name: "SystemUI-core-compat-config", + src: ":SystemUI-core", +} + filegroup { name: "AAA-src", srcs: ["tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java"], @@ -755,6 +762,7 @@ android_library { "kosmos", "testables", "androidx.test.rules", + "platform-compat-test-rules", ], libs: [ "android.test.runner.stubs.system", @@ -889,6 +897,7 @@ android_robolectric_test { static_libs: [ "RoboTestLibraries", "androidx.compose.runtime_runtime", + "platform-compat-test-rules", ], libs: [ "android.test.runner.impl", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 5b989cb6abc4..910f71276376 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1869,20 +1869,6 @@ flag { bug: "385194612" } -flag{ - name: "gsf_bouncer" - namespace: "systemui" - description: "Applies GSF font styles to Bouncer surfaces." - bug: "379364381" -} - -flag { - name: "gsf_quick_settings" - namespace: "systemui" - description: "Applies GSF font styles to Quick Settings surfaces." - bug: "379364381" -} - flag { name: "spatial_model_launcher_pushback" namespace: "systemui" @@ -1960,6 +1946,16 @@ flag { } flag { + name: "unfold_latency_tracking_fix" + namespace: "systemui" + description: "New implementation to track unfold latency that excludes broken cases" + bug: "390649568" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "ui_rich_ongoing_force_expanded" namespace: "systemui" description: "Force promoted notifications to always be expanded" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 910328dfa140..9c57efc24a22 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1705,38 +1705,15 @@ private fun Umo( contentScope: ContentScope?, modifier: Modifier = Modifier, ) { - val showNextActionLabel = stringResource(R.string.accessibility_action_label_umo_show_next) - val showPreviousActionLabel = - stringResource(R.string.accessibility_action_label_umo_show_previous) - - Box( - modifier = - modifier.thenIf(!viewModel.isEditMode) { - Modifier.semantics { - customActions = - listOf( - CustomAccessibilityAction(showNextActionLabel) { - viewModel.onShowNextMedia() - true - }, - CustomAccessibilityAction(showPreviousActionLabel) { - viewModel.onShowPreviousMedia() - true - }, - ) - } - } - ) { - if (SceneContainerFlag.isEnabled && contentScope != null) { - contentScope.MediaCarousel( - modifier = modifier.fillMaxSize(), - isVisible = true, - mediaHost = viewModel.mediaHost, - carouselController = viewModel.mediaCarouselController, - ) - } else { - UmoLegacy(viewModel, modifier) - } + if (SceneContainerFlag.isEnabled && contentScope != null) { + contentScope.MediaCarousel( + modifier = modifier.fillMaxSize(), + isVisible = true, + mediaHost = viewModel.mediaHost, + carouselController = viewModel.mediaCarouselController, + ) + } else { + UmoLegacy(viewModel, modifier) } } @@ -1747,7 +1724,7 @@ private fun UmoLegacy(viewModel: BaseCommunalViewModel, modifier: Modifier = Mod modifier .clip( shape = - RoundedCornerShape(dimensionResource(system_app_widget_background_radius)) + RoundedCornerShape(dimensionResource(R.dimen.notification_corner_radius)) ) .background(MaterialTheme.colorScheme.primary) .pointerInput(Unit) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt index 62aa31b49870..73a24257580c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.unit.times import androidx.window.layout.WindowMetricsCalculator import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_HEIGHT import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_WIDTH -import com.android.systemui.communal.util.WindowSizeUtils.MEDIUM_WIDTH /** * Renders a responsive [LazyHorizontalGrid] with dynamic columns and rows. Each cell will maintain @@ -267,9 +266,8 @@ fun calculateWindowSize(): DpSize { } private fun calculateNumCellsWidth(width: Dp) = - // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes when { - width >= MEDIUM_WIDTH -> 3 + width >= 900.dp -> 3 width >= COMPACT_WIDTH -> 2 else -> 1 } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index d7d4e1714aa6..09b8d178cc8e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -175,7 +175,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( viewModel: NotificationsPlaceholderViewModel, ) { - val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false) + val isSnoozable by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false) var scrollOffset by remember { mutableFloatStateOf(0f) } val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() } @@ -192,7 +192,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( ) } - val nestedScrollConnection = + val snoozeScrollConnection = object : NestedScrollConnection { override suspend fun onPreFling(available: Velocity): Velocity { if ( @@ -206,7 +206,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( } } - LaunchedEffect(isHeadsUp) { scrollOffset = 0f } + LaunchedEffect(isSnoozable) { scrollOffset = 0f } LaunchedEffect(scrollableState.isScrollInProgress) { if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) { @@ -230,10 +230,8 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( ), ) } - .thenIf(isHeadsUp) { - Modifier.nestedScroll(nestedScrollConnection) - .scrollable(orientation = Orientation.Vertical, state = scrollableState) - }, + .thenIf(isSnoozable) { Modifier.nestedScroll(snoozeScrollConnection) } + .scrollable(orientation = Orientation.Vertical, state = scrollableState), ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index db1358a5a28a..64f3cb13662a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -86,7 +86,7 @@ constructor( OverlayShade( panelElement = NotificationsShade.Elements.Panel, - panelAlignment = Alignment.TopStart, + alignmentOnWideScreens = Alignment.TopStart, modifier = modifier, onScrimClicked = viewModel::onScrimClicked, header = { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index cc58b8e13744..afdb3cbba60e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -128,7 +128,7 @@ constructor( ) OverlayShade( panelElement = QuickSettingsShade.Elements.Panel, - panelAlignment = Alignment.TopEnd, + alignmentOnWideScreens = Alignment.TopEnd, onScrimClicked = contentViewModel::onScrimClicked, header = { OverlayShadeHeader( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 619b4280d954..aa0d474ba41c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -204,7 +204,7 @@ fun SceneContainer( SceneTransitionLayout( state = state, modifier = modifier.fillMaxSize(), - swipeSourceDetector = viewModel.edgeDetector, + swipeSourceDetector = viewModel.swipeSourceDetector, ) { sceneByKey.forEach { (sceneKey, scene) -> scene( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 5dcec5b8836d..cdb1e2e53b09 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -59,7 +59,7 @@ import com.android.systemui.res.R @Composable fun ContentScope.OverlayShade( panelElement: ElementKey, - panelAlignment: Alignment, + alignmentOnWideScreens: Alignment, onScrimClicked: () -> Unit, modifier: Modifier = Modifier, header: @Composable () -> Unit, @@ -71,7 +71,7 @@ fun ContentScope.OverlayShade( Box( modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth), - contentAlignment = panelAlignment, + contentAlignment = if (isFullWidth) Alignment.TopCenter else alignmentOnWideScreens, ) { Panel( modifier = diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 2e5b5b56c982..aad1276d76e5 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -113,8 +113,8 @@ class DefaultClockProvider( companion object { // 750ms @ 120hz -> 90 frames of animation - // In practice, 45 looks good enough - const val NUM_CLOCK_FONT_ANIMATION_STEPS = 45 + // In practice, 30 looks good enough and limits our memory usage + const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30 val FLEX_TYPEFACE by lazy { // TODO(b/364680873): Move constant to config_clockFontFamily when shipping diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt new file mode 100644 index 000000000000..0c97750ba281 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 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.communal + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeTransitionModel +import com.android.systemui.keyguard.wakefulnessLifecycle +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.policy.keyguardStateController +import com.android.systemui.testKosmos +import com.android.systemui.util.kotlin.JavaAdapter +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceInactiveConditionTest : SysuiTestCase() { + private val kosmos = + testKosmos().useUnconfinedTestDispatcher().also { + whenever(it.wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_AWAKE + } + + private val Kosmos.underTest by + Kosmos.Fixture { + DeviceInactiveCondition( + applicationCoroutineScope, + keyguardStateController, + wakefulnessLifecycle, + keyguardUpdateMonitor, + keyguardInteractor, + JavaAdapter(applicationCoroutineScope), + ) + } + + @Test + fun asleep_conditionTrue() = + kosmos.runTest { + // Condition is false to start. + underTest.start() + assertThat(underTest.isConditionMet).isFalse() + + // Condition is true when device goes to sleep. + sleep() + assertThat(underTest.isConditionMet).isTrue() + } + + @Test + fun dozingAndAsleep_conditionFalse() = + kosmos.runTest { + // Condition is true when device is asleep. + underTest.start() + sleep() + assertThat(underTest.isConditionMet).isTrue() + + // Condition turns false after doze starts. + fakeKeyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.UNINITIALIZED, to = DozeStateModel.DOZE) + ) + assertThat(underTest.isConditionMet).isFalse() + } + + fun Kosmos.sleep() { + whenever(wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_ASLEEP + argumentCaptor<WakefulnessLifecycle.Observer>().apply { + verify(wakefulnessLifecycle).addObserver(capture()) + firstValue.onStartedGoingToSleep() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 433894b58350..85155157eda2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -78,7 +78,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.controller.mediaCarouselController -import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -121,7 +120,6 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost - @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var metricsLogger: CommunalMetricsLogger private val kosmos = testKosmos() @@ -163,8 +161,6 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) whenever(mediaHost.visible).thenReturn(true) - whenever(kosmos.mediaCarouselController.mediaCarouselScrollHandler) - .thenReturn(mediaCarouselScrollHandler) kosmos.powerInteractor.setAwakeForTest() @@ -907,20 +903,6 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun onShowPreviousMedia_scrollHandler_isCalled() = - testScope.runTest { - underTest.onShowPreviousMedia() - verify(mediaCarouselScrollHandler).scrollByStep(-1) - } - - @Test - fun onShowNextMedia_scrollHandler_isCalled() = - testScope.runTest { - underTest.onShowNextMedia() - verify(mediaCarouselScrollHandler).scrollByStep(1) - } - - @Test @EnableFlags(FLAG_BOUNCER_UI_REVAMP) fun uiIsBlurred_whenPrimaryBouncerIsShowing() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index e13f3f12c55a..7e93f5a8c9a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -20,19 +20,26 @@ import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.Flags.FLAG_SCENE_CONTAINER +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository @@ -56,11 +63,14 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -93,7 +103,10 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + FLAG_GLANCEABLE_HUB_V2, + ) } } @@ -107,6 +120,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT // Transition to DOZING and set the power interactor asleep. kosmos.powerInteractor.setAsleepForTest() + kosmos.setCommunalV2ConfigEnabled(true) runBlocking { kosmos.transitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, @@ -160,7 +174,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) - @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_GLANCEABLE_HUB_V2) fun testTransitionToLockscreen_onWake_canDream_glanceableHubAvailable() = kosmos.runTest { whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) @@ -179,7 +193,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() = kosmos.runTest { setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } clearInvocations(fakeCommunalSceneRepository) powerInteractor.setAwakeForTest() @@ -240,7 +264,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToGlanceableHub_onWakeup_ifAvailable() = kosmos.runTest { setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } // Device turns on. powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt index 8e1068226431..5882cff74eb6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -19,14 +19,20 @@ package com.android.systemui.keyguard.domain.interactor import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -46,6 +52,8 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy @@ -66,7 +74,10 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + FLAG_GLANCEABLE_HUB_V2, + ) .andSceneContainer() } } @@ -101,6 +112,7 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu ) reset(kosmos.transitionRepository) kosmos.setCommunalAvailable(true) + kosmos.setCommunalV2ConfigEnabled(true) } kosmos.underTest.start() } @@ -202,7 +214,17 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu reset(transitionRepository) setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } // Device wakes up. powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt index b66e2fe13e8a..47ca4b14a26f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt @@ -41,7 +41,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade @@ -275,20 +275,20 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { assertThat(downDestination?.transitionKey).isNull() } - val downFromTopRightDestination = + val downFromEndHalfDestination = userActions?.get( Swipe.Down( - fromSource = SceneContainerEdge.TopRight, + fromSource = SceneContainerArea.EndHalf, pointerCount = if (downWithTwoPointers) 2 else 1, ) ) when { - !isShadeTouchable -> assertThat(downFromTopRightDestination).isNull() - downWithTwoPointers -> assertThat(downFromTopRightDestination).isNull() + !isShadeTouchable -> assertThat(downFromEndHalfDestination).isNull() + downWithTwoPointers -> assertThat(downFromEndHalfDestination).isNull() else -> { - assertThat(downFromTopRightDestination) + assertThat(downFromEndHalfDestination) .isEqualTo(ShowOverlay(Overlays.QuickSettingsShade)) - assertThat(downFromTopRightDestination?.transitionKey).isNull() + assertThat(downFromEndHalfDestination?.transitionKey).isNull() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt index 46940297e673..d073cf1ac9db 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt @@ -16,11 +16,8 @@ package com.android.systemui.media.controls.ui.view -import android.content.res.Resources import android.testing.TestableLooper import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -28,19 +25,16 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock import org.mockito.Mockito.anyInt -import org.mockito.Mockito.eq -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -48,7 +42,6 @@ import org.mockito.kotlin.whenever class MediaCarouselScrollHandlerTest : SysuiTestCase() { private val carouselWidth = 1038 - private val settingsButtonWidth = 200 private val motionEventUp = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) @Mock lateinit var mediaCarousel: MediaScrollView @@ -60,9 +53,6 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { @Mock lateinit var falsingManager: FalsingManager @Mock lateinit var logSmartspaceImpression: (Boolean) -> Unit @Mock lateinit var logger: MediaUiEventLogger - @Mock lateinit var contentContainer: ViewGroup - @Mock lateinit var settingsButton: View - @Mock lateinit var resources: Resources lateinit var executor: FakeExecutor private val clock = FakeSystemClock() @@ -73,7 +63,6 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(clock) - whenever(mediaCarousel.contentContainer).thenReturn(contentContainer) mediaCarouselScrollHandler = MediaCarouselScrollHandler( mediaCarousel, @@ -85,9 +74,10 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { closeGuts, falsingManager, logSmartspaceImpression, - logger, + logger ) mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth + whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener) } @@ -138,107 +128,4 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) } - - @Test - fun testCarouselScrollByStep_scrollRight() { - setupMediaContainer(visibleIndex = 0) - - mediaCarouselScrollHandler.scrollByStep(1) - clock.advanceTime(DISMISS_DELAY) - executor.runAllReady() - - verify(mediaCarousel).smoothScrollTo(eq(carouselWidth), anyInt()) - } - - @Test - fun testCarouselScrollByStep_scrollLeft() { - setupMediaContainer(visibleIndex = 1) - - mediaCarouselScrollHandler.scrollByStep(-1) - clock.advanceTime(DISMISS_DELAY) - executor.runAllReady() - - verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) - } - - @Test - fun testCarouselScrollByStep_scrollRight_alreadyAtEnd() { - setupMediaContainer(visibleIndex = 1) - - mediaCarouselScrollHandler.scrollByStep(1) - clock.advanceTime(DISMISS_DELAY) - executor.runAllReady() - - verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) - verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) - } - - @Test - fun testCarouselScrollByStep_scrollLeft_alreadyAtStart() { - setupMediaContainer(visibleIndex = 0) - - mediaCarouselScrollHandler.scrollByStep(-1) - clock.advanceTime(DISMISS_DELAY) - executor.runAllReady() - - verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) - verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) - } - - @Test - fun testCarouselScrollByStep_scrollLeft_alreadyAtStart_isRTL() { - setupMediaContainer(visibleIndex = 0) - whenever(mediaCarousel.isLayoutRtl).thenReturn(true) - - mediaCarouselScrollHandler.scrollByStep(-1) - clock.advanceTime(DISMISS_DELAY) - executor.runAllReady() - - verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) - verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) - } - - @Test - fun testCarouselScrollByStep_scrollRight_alreadyAtEnd_isRTL() { - setupMediaContainer(visibleIndex = 1) - whenever(mediaCarousel.isLayoutRtl).thenReturn(true) - - mediaCarouselScrollHandler.scrollByStep(1) - clock.advanceTime(DISMISS_DELAY) - executor.runAllReady() - - verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) - verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) - } - - @Test - fun testScrollByStep_noScroll_notDismissible() { - setupMediaContainer(visibleIndex = 1, showsSettingsButton = false) - - mediaCarouselScrollHandler.scrollByStep(1) - clock.advanceTime(DISMISS_DELAY) - executor.runAllReady() - - verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) - verify(mediaCarousel, never()).animationTargetX = anyFloat() - } - - private fun setupMediaContainer(visibleIndex: Int, showsSettingsButton: Boolean = true) { - whenever(contentContainer.childCount).thenReturn(2) - val child1: View = mock() - val child2: View = mock() - whenever(child1.left).thenReturn(0) - whenever(child2.left).thenReturn(carouselWidth) - whenever(contentContainer.getChildAt(0)).thenReturn(child1) - whenever(contentContainer.getChildAt(1)).thenReturn(child2) - - whenever(settingsButton.width).thenReturn(settingsButtonWidth) - whenever(settingsButton.context).thenReturn(context) - whenever(settingsButton.resources).thenReturn(resources) - whenever(settingsButton.resources.getDimensionPixelSize(anyInt())).thenReturn(20) - mediaCarouselScrollHandler.onSettingsButtonUpdated(settingsButton) - - mediaCarouselScrollHandler.visibleMediaIndex = visibleIndex - mediaCarouselScrollHandler.showsSettingsButton = showsSettingsButton - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt index 52b9e47e6d3d..52a0a5445002 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt @@ -30,7 +30,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -71,13 +71,13 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { } @Test - fun downFromTopRight_switchesToQuickSettingsShade() = + fun downFromTopEnd_switchesToQuickSettingsShade() = testScope.runTest { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) val action = - (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) as? ShowOverlay) + (actions?.get(Swipe.Down(fromSource = SceneContainerArea.EndHalf)) as? ShowOverlay) assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade) val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some assertThat(overlaysToHide).isNotNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt index 264eda5a07eb..668c606677ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.settings.userFileManager import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository @@ -76,11 +77,11 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { @Test fun setLargeTilesSpecs_inSharedPreferences() { val setA = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setA.toTileSpecs()) + underTest.writeLargeTileSpecs(setA.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA) val setB = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setB.toTileSpecs()) + underTest.writeLargeTileSpecs(setB.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB) } @@ -92,12 +93,12 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) val setA = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setA.toTileSpecs()) + underTest.writeLargeTileSpecs(setA.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA) fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) val setB = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setB.toTileSpecs()) + underTest.writeLargeTileSpecs(setB.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB) fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) @@ -106,7 +107,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { } @Test - fun setInitialTilesFromSettings_noLargeTiles_tilesSet() = + fun setUpgradePathFromSettings_noLargeTiles_tilesSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -117,14 +118,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse() - underTest.setInitialLargeTilesSpecs(tiles, PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(tiles) } } @Test - fun setInitialTilesFromSettings_alreadyLargeTiles_tilesNotSet() = + fun setUpgradePathFromSettings_alreadyLargeTiles_tilesNotSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -133,14 +137,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) setLargeTilesSpecsInSharedPreferences(setOf("tileC")) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEqualTo(setOf("tileC").toTileSpecs()) } } @Test - fun setInitialTilesFromSettings_emptyLargeTiles_tilesNotSet() = + fun setUpgradePathFromSettings_emptyLargeTiles_tilesNotSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -149,14 +156,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) setLargeTilesSpecsInSharedPreferences(emptySet()) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEmpty() } } @Test - fun setInitialTilesFromSettings_nonCurrentUser_tilesSetForCorrectUser() = + fun setUpgradePathFromSettings_nonCurrentUser_tilesSetForCorrectUser() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -164,7 +174,10 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setUserInfos(USERS) fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) @@ -174,7 +187,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { } @Test - fun setInitialTiles_afterDefaultRead_noSetOnRepository_initialTilesCorrect() = + fun setUpgradePath_afterDefaultRead_noSetOnRepository_initialTilesCorrect() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -186,14 +199,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(currentLargeTiles).isNotEmpty() val tiles = setOf("tileA", "tileB") - underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(tiles.toTileSpecs()) } } @Test - fun setInitialTiles_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() = + fun setUpgradePath_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -204,15 +220,80 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(currentLargeTiles).isNotEmpty() - underTest.setLargeTilesSpecs(setOf(TileSpec.create("tileC"))) + underTest.writeLargeTileSpecs(setOf(TileSpec.create("tileC"))) val tiles = setOf("tileA", "tileB") - underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(setOf(TileSpec.create("tileC"))) } } + @Test + fun setTilesRestored_noLargeTiles_tilesSet() = + with(kosmos) { + testScope.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + val tiles = setOf("tileA", "tileB").toTileSpecs() + + assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse() + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.RestoreFromBackup(tiles), + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + } + + @Test + fun setDefaultTilesInitial_defaultSetLarge() = + with(kosmos) { + testScope.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.DefaultSet, + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + } + + @Test + fun setTilesRestored_afterDefaultSet_tilesSet() = + with(kosmos) { + testScope.runTest { + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.DefaultSet, + PRIMARY_USER_ID, + ) + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + val tiles = setOf("tileA", "tileB").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.RestoreFromBackup(tiles), + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + } + private fun getSharedPreferences(): SharedPreferences = with(kosmos) { return userFileManager.getSharedPreferences( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt new file mode 100644 index 000000000000..f3c1f0c9dba8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.domain + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.mainResources +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.common.shared.model.PackageChangeModel.Empty.packageName +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository +import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository +import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor +import com.android.systemui.qs.pipeline.data.repository.DefaultTilesQSHostRepository +import com.android.systemui.qs.pipeline.data.repository.defaultTilesRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath +import com.android.systemui.settings.userFileManager +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.userRepository +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LargeTilesUpgradePathsTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { defaultTilesRepository = DefaultTilesQSHostRepository(mainResources) } + + private val defaultTiles = kosmos.defaultTilesRepository.defaultTiles.toSet() + + private val underTest = kosmos.qsPreferencesInteractor + + private val Kosmos.userId + get() = userRepository.getSelectedUserInfo().id + + private val Kosmos.intent + get() = + Intent(ACTION_RESTORE_FINISHED).apply { + `package` = packageName + putExtra(Intent.EXTRA_USER_ID, kosmos.userId) + flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY + } + + /** + * This test corresponds to the case of a fresh start. + * + * The resulting large tiles are the default set of large tiles. + */ + @Test + fun defaultTiles_noDataInSharedPreferences_defaultLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + + /** + * This test corresponds to a user that upgraded in place from a build that didn't support large + * tiles to one that does. The current tiles of the user are read from settings. + * + * The resulting large tiles are those that were read from Settings. + */ + @Test + fun upgradeInPlace_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a fresh start, and then the user restarts the device, without ever + * having modified the set of large tiles. + * + * The resulting large tiles are the default large tiles that were set on the fresh start + */ + @Test + fun defaultSet_restartDevice_largeTilesDontChange() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + // User restarts the device, this will send a read from settings with the default + // set of tiles + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(defaultTiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + + /** + * This test corresponds to a fresh start, following the user changing the sizes of some tiles. + * After that, the user restarts the device. + * + * The resulting set of large tiles are those that the user determined before restarting the + * device. + */ + @Test + fun defaultSet_someSizeChanges_restart_correctSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + underTest.setLargeTilesSpecs(largeTiles!! + setOf("a", "b").toTileSpecs()) + val largeTilesBeforeRestart = largeTiles!! + + // Restart + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(defaultTiles), + userId, + ) + assertThat(largeTiles).isEqualTo(largeTilesBeforeRestart) + } + + /** + * This test corresponds to a user that upgraded, and after that performed some size changes. + * After that, the user restarts the device. + * + * The resulting set of large tiles are those that the user determined before restarting the + * device. + */ + @Test + fun readFromSettings_changeSizes_restart_newLargeSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val readTiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + underTest.setLargeTilesSpecs(emptySet()) + + assertThat(largeTiles).isEmpty() + + // Restart + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + assertThat(largeTiles).isEmpty() + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. Note that there's no file in SharedPreferences to + * restore. + * + * The resulting set of large tiles are those that were restored from the backup. + */ + @Test + fun restoreFromBackup_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. However, the restore happens after SystemUI's + * initialization has set the tiles to default. Note that there's no file in SharedPreferences + * to restore. + * + * The resulting set of large tiles are those that were restored from the backup. + */ + @Test + fun restoreFromBackup_afterDefault_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a user that restored from a build that supported different sizes + * tiles. First the list of tiles is restored in Settings and then a file containing some large + * tiles overrides the current shared preferences file + * + * The resulting set of large tiles are those that were restored from the shared preferences + * backup (and not the full list). + */ + @Test + fun restoreFromBackup_thenRestoreOfSharedPrefs_sharedPrefsAreLarge() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + val tilesFromBackupOfSharedPrefs = setOf("a") + setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs()) + } + + /** + * This test corresponds to a user that restored from a build that supported different sizes + * tiles. However, this restore of settings happened after SystemUI's restore of the SharedPrefs + * containing the user's previous selections to large/small tiles. + * + * The resulting set of large tiles are those that were restored from the shared preferences + * backup (and not the full list). + */ + @Test + fun restoreFromBackup_afterRestoreOfSharedPrefs_sharedPrefsAreLarge() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + val tilesFromBackupOfSharedPrefs = setOf("a") + + setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs()) + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. After that, the user modifies the size of some tiles + * and then restarts the device. + * + * The resulting set of large tiles are those after the user modifications. + */ + @Test + fun restoreFromBackup_changeSizes_restart_newLargeSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val readTiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(readTiles), + userId, + ) + underTest.setLargeTilesSpecs(emptySet()) + + assertThat(largeTiles).isEmpty() + + // Restart + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + assertThat(largeTiles).isEmpty() + } + + private companion object { + private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" + + private fun Kosmos.getSharedPreferences(): SharedPreferences = + userFileManager.getSharedPreferences( + QSPreferencesRepository.FILE_NAME, + Context.MODE_PRIVATE, + userRepository.getSelectedUserInfo().id, + ) + + private fun Kosmos.setLargeTilesSpecsInSharedPreferences(specs: Set<String>) { + getSharedPreferences().edit().putStringSet(LARGE_TILES_SPECS_KEY, specs).apply() + } + + private fun Kosmos.getLargeTilesSpecsFromSharedPreferences(): Set<String> { + return getSharedPreferences().getStringSet(LARGE_TILES_SPECS_KEY, emptySet())!! + } + + private fun Set<String>.toTileSpecs(): Set<TileSpec> { + return map { TileSpec.create(it) }.toSet() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt index 79acfdaa415b..9838bcb86684 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt @@ -66,7 +66,7 @@ class IconTilesInteractorTest : SysuiTestCase() { runCurrent() // Resize it to large - qsPreferencesRepository.setLargeTilesSpecs(setOf(spec)) + qsPreferencesRepository.writeLargeTileSpecs(setOf(spec)) runCurrent() // Assert that the new tile was added to the large tiles set diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index 4b8cd3742bff..d9b3926fa215 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.res.R import com.android.systemui.retail.data.repository.FakeRetailModeRepository @@ -242,9 +243,12 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { storeTilesForUser(startingTiles, userId) val tiles by collectLastValue(underTest.tilesSpecs(userId)) - val tilesRead by collectLastValue(underTest.tilesReadFromSetting.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) - assertThat(tilesRead).isEqualTo(startingTiles.toTileSpecs().toSet() to userId) + assertThat(tilesRead) + .isEqualTo( + TilesUpgradePath.ReadFromSettings(startingTiles.toTileSpecs().toSet()) to userId + ) } @Test @@ -258,13 +262,13 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { val tiles10 by collectLastValue(underTest.tilesSpecs(10)) val tiles11 by collectLastValue(underTest.tilesSpecs(11)) - val tilesRead by collectValues(underTest.tilesReadFromSetting.consumeAsFlow()) + val tilesRead by collectValues(underTest.tilesUpgradePath.consumeAsFlow()) assertThat(tilesRead).hasSize(2) assertThat(tilesRead) .containsExactly( - startingTiles10.toTileSpecs().toSet() to 10, - startingTiles11.toTileSpecs().toSet() to 11, + TilesUpgradePath.ReadFromSettings(startingTiles10.toTileSpecs().toSet()) to 10, + TilesUpgradePath.ReadFromSettings(startingTiles11.toTileSpecs().toSet()) to 11, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt index 1945f750efaf..29bd18d3f3a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt @@ -7,8 +7,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.pipeline.data.model.RestoreData -import com.android.systemui.qs.pipeline.data.repository.UserTileSpecRepositoryTest.Companion.toTilesSet import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat @@ -352,11 +352,11 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { @Test fun noSettingsStored_noTilesReadFromSettings() = testScope.runTest { - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) val tiles by collectLastValue(underTest.tiles()) assertThat(tiles).isEqualTo(getDefaultTileSpecs()) - assertThat(tilesRead).isEqualTo(null) + assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet) } @Test @@ -365,19 +365,20 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { val storedTiles = "a,b" storeTiles(storedTiles) val tiles by collectLastValue(underTest.tiles()) - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) - assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet()) + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet())) } @Test fun noSettingsStored_tilesChanged_tilesReadFromSettingsNotChanged() = testScope.runTest { - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) val tiles by collectLastValue(underTest.tiles()) underTest.addTile(TileSpec.create("a")) - assertThat(tilesRead).isEqualTo(null) + assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet) } @Test @@ -386,10 +387,34 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { val storedTiles = "a,b" storeTiles(storedTiles) val tiles by collectLastValue(underTest.tiles()) - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) underTest.addTile(TileSpec.create("c")) - assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet()) + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet())) + } + + @Test + fun tilesRestoredFromBackup() = + testScope.runTest { + val specsBeforeRestore = "a,b,c,d,e" + val restoredSpecs = "a,c,d,f" + val autoAddedBeforeRestore = "b,d" + val restoredAutoAdded = "d,e" + + storeTiles(specsBeforeRestore) + val tiles by collectLastValue(underTest.tiles()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) + runCurrent() + + val restoreData = + RestoreData(restoredSpecs.toTileSpecs(), restoredAutoAdded.toTilesSet(), USER) + underTest.reconcileRestore(restoreData, autoAddedBeforeRestore.toTilesSet()) + runCurrent() + + val expected = "a,b,c,d,f" + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.RestoreFromBackup(expected.toTilesSet())) } private fun getDefaultTileSpecs(): List<TileSpec> { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt index df2dd99c779e..b98059a1fe90 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt @@ -31,7 +31,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -84,13 +84,14 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() { } @Test - fun downFromTopLeft_switchesToNotificationsShade() = + fun downFromTopStart_switchesToNotificationsShade() = testScope.runTest { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) val action = - (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) as? ShowOverlay) + (actions?.get(Swipe.Down(fromSource = SceneContainerArea.StartHalf)) + as? ShowOverlay) assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade) val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some assertThat(overlaysToHide).isNotNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index a0d86f27b9b8..80c7026b0cea 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteract import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent @@ -60,6 +61,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -713,4 +718,43 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(originalScene) assertThat(currentOverlays).isEmpty() } + + @Test + fun changeScene_notifiesAboutToChangeListener() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + // Unlock so transitioning to the Gone scene becomes possible. + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + underTest.changeScene(toScene = Scenes.Gone, loggingReason = "") + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Gone) + + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + + underTest.changeScene( + toScene = Scenes.Lockscreen, + sceneState = KeyguardState.AOD, + loggingReason = "", + ) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + + verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD) + } + + @Test + fun changeScene_noOp_whenFromAndToAreTheSame() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + + underTest.changeScene(toScene = checkNotNull(currentScene), loggingReason = "") + + verify(processor, never()).onSceneAboutToChange(any(), any()) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt new file mode 100644 index 000000000000..a09e5cd9de9b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2025 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.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.BottomEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartHalf +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneContainerSwipeDetectorTest : SysuiTestCase() { + + private val edgeSize = 40 + private val screenWidth = 800 + private val screenHeight = 600 + + private val underTest = SceneContainerSwipeDetector(edgeSize = edgeSize.dp) + + @Test + fun source_noEdge_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = screenWidth / 2 - 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeVerticallyOnTopLeft_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeHorizontallyOnTopLeft_detectsLeftEdge() { + val detectedEdge = swipeHorizontallyFrom(x = 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(LeftEdge) + } + + @Test + fun source_swipeVerticallyOnTopRight_detectsRightHalf() { + val detectedEdge = swipeVerticallyFrom(x = screenWidth - 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(RightHalf) + } + + @Test + fun source_swipeHorizontallyOnTopRight_detectsRightEdge() { + val detectedEdge = swipeHorizontallyFrom(x = screenWidth - 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(RightEdge) + } + + @Test + fun source_swipeVerticallyToLeftOfSplit_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeVerticallyToRightOfSplit_detectsRightHalf() { + val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(RightHalf) + } + + @Test + fun source_swipeVerticallyOnBottom_detectsBottomEdge() { + val detectedEdge = + swipeVerticallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize / 2)) + assertThat(detectedEdge).isEqualTo(BottomEdge) + } + + @Test + fun source_swipeHorizontallyOnBottom_detectsLeftHalf() { + val detectedEdge = + swipeHorizontallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize - 1)) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeHorizontallyOnLeft_detectsLeftEdge() { + val detectedEdge = swipeHorizontallyFrom(x = edgeSize - 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(LeftEdge) + } + + @Test + fun source_swipeVerticallyOnLeft_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = edgeSize - 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeHorizontallyOnRight_detectsRightEdge() { + val detectedEdge = + swipeHorizontallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(RightEdge) + } + + @Test + fun source_swipeVerticallyOnRight_detectsRightHalf() { + val detectedEdge = swipeVerticallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(RightHalf) + } + + @Test + fun resolve_startEdgeInLtr_resolvesLeftEdge() { + val resolvedEdge = StartEdge.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(LeftEdge) + } + + @Test + fun resolve_startEdgeInRtl_resolvesRightEdge() { + val resolvedEdge = StartEdge.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(RightEdge) + } + + @Test + fun resolve_endEdgeInLtr_resolvesRightEdge() { + val resolvedEdge = EndEdge.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(RightEdge) + } + + @Test + fun resolve_endEdgeInRtl_resolvesLeftEdge() { + val resolvedEdge = EndEdge.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(LeftEdge) + } + + @Test + fun resolve_startHalfInLtr_resolvesLeftHalf() { + val resolvedEdge = StartHalf.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(LeftHalf) + } + + @Test + fun resolve_startHalfInRtl_resolvesRightHalf() { + val resolvedEdge = StartHalf.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(RightHalf) + } + + @Test + fun resolve_endHalfInLtr_resolvesRightHalf() { + val resolvedEdge = EndHalf.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(RightHalf) + } + + @Test + fun resolve_endHalfInRtl_resolvesLeftHalf() { + val resolvedEdge = EndHalf.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(LeftHalf) + } + + private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? { + return swipeFrom(x, y, Orientation.Vertical) + } + + private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? { + return swipeFrom(x, y, Orientation.Horizontal) + } + + private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerArea.Resolved? { + return underTest.source( + layoutSize = IntSize(width = screenWidth, height = screenHeight), + position = IntOffset(x, y), + density = Density(1f), + orientation = orientation, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index 30d9f73d7441..adaebbd27986 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -55,6 +56,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer @@ -324,7 +326,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { kosmos.enableSingleShade() assertThat(shadeMode).isEqualTo(ShadeMode.Single) - assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector) } @Test @@ -334,26 +336,28 @@ class SceneContainerViewModelTest : SysuiTestCase() { kosmos.enableSplitShade() assertThat(shadeMode).isEqualTo(ShadeMode.Split) - assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector) } @Test - fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() = + fun edgeDetector_dualShade_narrowScreen_usesSceneContainerSwipeDetector() = testScope.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = false) assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + assertThat(underTest.swipeSourceDetector) + .isInstanceOf(SceneContainerSwipeDetector::class.java) } @Test - fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() = + fun edgeDetector_dualShade_wideScreen_usesSceneContainerSwipeDetector() = testScope.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = true) assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + assertThat(underTest.swipeSourceDetector) + .isInstanceOf(SceneContainerSwipeDetector::class.java) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt deleted file mode 100644 index 3d76d280b2cc..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.scene.ui.viewmodel - -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart -import com.google.common.truth.Truth.assertThat -import kotlin.test.assertFailsWith -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class SplitEdgeDetectorTest : SysuiTestCase() { - - private val edgeSize = 40 - private val screenWidth = 800 - private val screenHeight = 600 - - private var edgeSplitFraction = 0.7f - - private val underTest = - SplitEdgeDetector( - topEdgeSplitFraction = { edgeSplitFraction }, - edgeSize = edgeSize.dp, - ) - - @Test - fun source_noEdge_detectsNothing() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth / 2, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun source_swipeVerticallyOnTopLeft_detectsTopLeft() { - val detectedEdge = - swipeVerticallyFrom( - x = 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopLeft) - } - - @Test - fun source_swipeHorizontallyOnTopLeft_detectsLeft() { - val detectedEdge = - swipeHorizontallyFrom( - x = 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(Left) - } - - @Test - fun source_swipeVerticallyOnTopRight_detectsTopRight() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth - 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopRight) - } - - @Test - fun source_swipeHorizontallyOnTopRight_detectsRight() { - val detectedEdge = - swipeHorizontallyFrom( - x = screenWidth - 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(Right) - } - - @Test - fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() { - val detectedEdge = - swipeVerticallyFrom( - x = (screenWidth * edgeSplitFraction).toInt() - 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopLeft) - } - - @Test - fun source_swipeVerticallyToRightOfSplit_detectsTopRight() { - val detectedEdge = - swipeVerticallyFrom( - x = (screenWidth * edgeSplitFraction).toInt() + 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopRight) - } - - @Test - fun source_edgeSplitFractionUpdatesDynamically() { - val middleX = (screenWidth * 0.5f).toInt() - val topY = 0 - - // Split closer to the right; middle of screen is considered "left". - edgeSplitFraction = 0.6f - assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft) - - // Split closer to the left; middle of screen is considered "right". - edgeSplitFraction = 0.4f - assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight) - - // Illegal fraction. - edgeSplitFraction = 1.2f - assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } - - // Illegal fraction. - edgeSplitFraction = -0.3f - assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } - } - - @Test - fun source_swipeVerticallyOnBottom_detectsBottom() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth / 3, - y = screenHeight - (edgeSize / 2), - ) - assertThat(detectedEdge).isEqualTo(Bottom) - } - - @Test - fun source_swipeHorizontallyOnBottom_detectsNothing() { - val detectedEdge = - swipeHorizontallyFrom( - x = screenWidth / 3, - y = screenHeight - (edgeSize - 1), - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun source_swipeHorizontallyOnLeft_detectsLeft() { - val detectedEdge = - swipeHorizontallyFrom( - x = edgeSize - 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isEqualTo(Left) - } - - @Test - fun source_swipeVerticallyOnLeft_detectsNothing() { - val detectedEdge = - swipeVerticallyFrom( - x = edgeSize - 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun source_swipeHorizontallyOnRight_detectsRight() { - val detectedEdge = - swipeHorizontallyFrom( - x = screenWidth - edgeSize + 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isEqualTo(Right) - } - - @Test - fun source_swipeVerticallyOnRight_detectsNothing() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth - edgeSize + 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun resolve_startInLtr_resolvesLeft() { - val resolvedEdge = Start.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(Left) - } - - @Test - fun resolve_startInRtl_resolvesRight() { - val resolvedEdge = Start.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(Right) - } - - @Test - fun resolve_endInLtr_resolvesRight() { - val resolvedEdge = End.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(Right) - } - - @Test - fun resolve_endInRtl_resolvesLeft() { - val resolvedEdge = End.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(Left) - } - - @Test - fun resolve_topStartInLtr_resolvesTopLeft() { - val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(TopLeft) - } - - @Test - fun resolve_topStartInRtl_resolvesTopRight() { - val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(TopRight) - } - - @Test - fun resolve_topEndInLtr_resolvesTopRight() { - val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(TopRight) - } - - @Test - fun resolve_topEndInRtl_resolvesTopLeft() { - val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(TopLeft) - } - - private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { - return swipeFrom(x, y, Orientation.Vertical) - } - - private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { - return swipeFrom(x, y, Orientation.Horizontal) - } - - private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? { - return underTest.source( - layoutSize = IntSize(width = screenWidth, height = screenHeight), - position = IntOffset(x, y), - density = Density(1f), - orientation = orientation, - ) - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 816df0102940..20637cd4af33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -61,14 +61,14 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.addNotif +import com.android.systemui.statusbar.notification.data.repository.addNotifs import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel -import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization -import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -93,7 +93,6 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { private val screenRecordState = kosmos.screenRecordRepository.screenRecordState private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState - private val callRepo = kosmos.ongoingCallRepository private val activeNotificationListRepository = kosmos.activeNotificationListRepository private val mockSystemUIDialog = mock<SystemUIDialog>() @@ -132,7 +131,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -145,7 +144,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -178,7 +177,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -191,7 +190,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -224,7 +223,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + addOngoingCallState("call") val latest by collectLastValue(underTest.primaryChip) @@ -237,9 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -255,16 +252,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = "call", - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(callNotificationKey) val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -281,7 +269,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chipsLegacy_oneChip_notSquished() = kosmos.runTest { - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState() val latest by collectLastValue(underTest.chipsLegacy) @@ -294,17 +282,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chips_oneChip_notSquished() = kosmos.runTest { - val callNotificationKey = "call" - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState() val latest by collectLastValue(underTest.chips) @@ -315,10 +293,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test - fun chipsLegacy_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() = + fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) @@ -329,12 +307,28 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_twoTimerChips_isSmallPortrait_bothSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + val latest by collectLastValue(underTest.chips) + + // Squished chips are icon only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) @@ -346,6 +340,23 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000) + addOngoingCallState(key = "call") + + val latest by collectLastValue(underTest.chips) + + // The screen record countdown isn't squished to icon-only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java) + // But the call chip *is* squished + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = @@ -354,7 +365,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // WHEN there's only one chip screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") // The screen record isn't squished because it's the only one assertThat(latest!!.primary) @@ -363,7 +374,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) // WHEN there's 2 chips - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") // THEN they both become squished assertThat(latest!!.primary) @@ -382,12 +393,44 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + // WHEN there's only one chip + screenRecordState.value = ScreenRecordModel.Recording + removeOngoingCallState(key = "call") + + // The screen record isn't squished because it's the only one + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + + // WHEN there's 2 chips + addOngoingCallState(key = "call") + + // THEN they both become squished + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + // But the call chip *is* squished + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + + // WHEN we go back down to 1 chip + screenRecordState.value = ScreenRecordModel.DoingNothing + + // THEN the remaining chip unsquishes + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_twoChips_isLandscape_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") // WHEN we're in landscape val config = @@ -405,12 +448,35 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_twoChips_isLandscape_notSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + // WHEN we're in landscape + val config = + Configuration(kosmos.mainResources.configuration).apply { + orientation = Configuration.ORIENTATION_LANDSCAPE + } + kosmos.fakeConfigurationRepository.onConfigurationChange(config) + + val latest by collectLastValue(underTest.chips) + + // THEN the chips aren't squished (squished chips would be icon only) + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_twoChips_isLargeScreen_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") // WHEN we're on a large screen kosmos.displayStateRepository.setIsLargeScreen(true) @@ -424,25 +490,19 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @Test @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun chips_twoChips_chipsModernizationEnabled_notSquished() = + @Test + fun chips_twoChips_isLargeScreen_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = "call", - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(key = "call") + + // WHEN we're on a large screen + kosmos.displayStateRepository.setIsLargeScreen(true) val latest by collectLastValue(underTest.chips) - // Squished chips would be icon only + // THEN the chips aren't squished (squished chips would be icon only) assertThat(latest!!.active[0]) .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat(latest!!.active[1]) @@ -455,7 +515,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -469,7 +529,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -510,7 +570,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -525,9 +585,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -545,16 +603,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -575,9 +624,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting val callNotificationKey = "call" - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.primaryChip) @@ -593,9 +640,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -614,16 +659,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -837,12 +873,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chips) val callNotificationKey = "call" - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) val firstIcon = createStatusBarIconViewOrNull() - setNotifs( + activeNotificationListRepository.addNotifs( listOf( activeNotificationModel( key = "firstNotif", @@ -874,14 +908,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" val firstIcon = createStatusBarIconViewOrNull() val secondIcon = createStatusBarIconViewOrNull() - setNotifs( + addOngoingCallState(key = callNotificationKey) + activeNotificationListRepository.addNotifs( listOf( activeNotificationModel( - key = callNotificationKey, - whenTime = 499, - callType = CallType.Ongoing, - ), - activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, promotedContent = @@ -913,17 +943,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) @@ -942,20 +968,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" val notifIcon = createStatusBarIconViewOrNull() screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - whenTime = 499, - callType = CallType.Ongoing, - ), - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ), + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = notifIcon, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) + addOngoingCallState(key = callNotificationKey) assertThat(latest!!.active.size).isEqualTo(2) assertIsScreenRecordChip(latest!!.active[0]) @@ -982,7 +1002,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) // And everything else hidden - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing @@ -991,9 +1011,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest, context, notifIcon, "notif") // WHEN the higher priority call chip is added - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) // THEN the higher priority call chip is used assertIsCallChip(latest, callNotificationKey) @@ -1024,17 +1042,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) val notifIcon = createStatusBarIconViewOrNull() - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = notifIcon, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) @@ -1056,7 +1070,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsCallChip(latest, callNotificationKey) // WHEN the higher priority call is removed - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) // THEN the lower priority notif is used assertIsNotifChip(latest, context, notifIcon, "notif") @@ -1069,17 +1083,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" // Start with just the lowest priority chip shown val notifIcon = createStatusBarIconViewOrNull() - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = notifIcon, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) // And everything else hidden - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing @@ -1092,9 +1104,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) // WHEN the higher priority call chip is added - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) // THEN the higher priority call chip is used as primary and notif is demoted to // secondary @@ -1125,7 +1135,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // WHEN screen record and call is dropped screenRecordState.value = ScreenRecordModel.DoingNothing - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) // THEN media projection and notif remain assertIsShareToAppChip(latest!!.primary) @@ -1172,21 +1182,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) // WHEN the higher priority call chip is added - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ), - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ), - ) - ) + addOngoingCallState(key = callNotificationKey) // THEN the higher priority call chip and notif are active in that order assertThat(latest!!.active.size).isEqualTo(2) @@ -1372,7 +1368,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -1399,7 +1395,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) screenRecordState.value = ScreenRecordModel.DoingNothing - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt index 96c9dc83a6bd..d570f18e35d8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -20,6 +20,8 @@ import android.os.PowerManager 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.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.Kosmos @@ -28,9 +30,13 @@ import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testCase import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.shade.domain.interactor.enableDualShade +import com.android.systemui.shade.domain.interactor.enableSingleShade +import com.android.systemui.shade.domain.interactor.enableSplitShade import com.android.systemui.statusbar.lockscreenShadeTransitionController import com.android.systemui.statusbar.phone.screenOffAnimationController 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.eq @@ -120,4 +126,48 @@ class NotificationShelfViewModelTest : SysuiTestCase() { assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE) verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true)) } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_splitShade_true() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableSplitShade() + + assertThat(isShelfAlignedToEnd).isTrue() + } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_singleShade_false() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableSingleShade() + + assertThat(isShelfAlignedToEnd).isFalse() + } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_dualShade_wideScreen_false() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableDualShade(wideLayout = true) + + assertThat(isShelfAlignedToEnd).isFalse() + } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_dualShade_narrowScreen_false() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableDualShade(wideLayout = false) + + assertThat(isShelfAlignedToEnd).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index f0823e2f645e..c48287c32120 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -35,8 +35,8 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.setNoCallState -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.setOngoingCallState +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -78,8 +78,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { val testIntent: PendingIntent = mock() val testPromotedContent = PromotedNotificationContentModel.Builder("promotedCall").build() - setOngoingCallState( - kosmos = this, + addOngoingCallState( key = "promotedCall", startTimeMs = 1000L, statusBarChipIconView = testIconView, @@ -100,8 +99,8 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.ongoingCallState) - setOngoingCallState(kosmos = this) - setNoCallState(kosmos = this) + addOngoingCallState(key = "testKey") + removeOngoingCallState(key = "testKey") assertThat(latest).isInstanceOf(OngoingCallModel.NoCall::class.java) } @@ -112,7 +111,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true val latest by collectLastValue(underTest.ongoingCallState) - setOngoingCallState(kosmos = this, uid = UID) + addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) } @@ -123,7 +122,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false val latest by collectLastValue(underTest.ongoingCallState) - setOngoingCallState(kosmos = this, uid = UID) + addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) } @@ -135,7 +134,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { // Start with notification and app not visible kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - setOngoingCallState(kosmos = this, uid = UID) + addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) // App becomes visible @@ -161,7 +160,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.fakeStatusBarWindowControllerStore.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) - setOngoingCallState(kosmos = this) + addOngoingCallState() assertThat(isStatusBarRequired).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() @@ -183,9 +182,9 @@ class OngoingCallInteractorTest : SysuiTestCase() { .ongoingProcessRequiresStatusBarVisible ) - setOngoingCallState(kosmos = this) + addOngoingCallState(key = "testKey") - setNoCallState(kosmos = this) + removeOngoingCallState(key = "testKey") assertThat(isStatusBarRequired).isFalse() assertThat(requiresStatusBarVisibleInRepository).isFalse() @@ -210,7 +209,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - setOngoingCallState(kosmos = this, uid = UID) + addOngoingCallState(uid = UID) assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(requiresStatusBarVisibleInRepository).isTrue() @@ -232,7 +231,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) // Set up notification but not in fullscreen kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - setOngoingCallState(kosmos = this) + addOngoingCallState() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) verify(kosmos.swipeStatusBarAwayGestureHandler, never()) @@ -246,7 +245,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - setOngoingCallState(kosmos = this) + addOngoingCallState() assertThat(isGestureListeningEnabled).isTrue() verify(kosmos.swipeStatusBarAwayGestureHandler) @@ -260,7 +259,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - setOngoingCallState(kosmos = this) + addOngoingCallState() clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) @@ -287,7 +286,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { ) // Start with an ongoing call (which should set status bar required) - setOngoingCallState(kosmos = this) + addOngoingCallState() assertThat(isStatusBarRequiredForOngoingCall).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index fecf1fd2f222..354edac75452 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -20,9 +20,12 @@ import android.content.Context import android.content.res.Resources import android.hardware.devicestate.DeviceStateManager import android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD +import android.os.PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl @@ -44,8 +47,10 @@ import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_OFF import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.COOL_DOWN_DURATION import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor @@ -56,11 +61,13 @@ import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.asExecutor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -73,6 +80,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +import org.mockito.kotlin.times @RunWith(AndroidJUnit4::class) @SmallTest @@ -88,6 +96,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private val animationStatusRepository = kosmos.fakeAnimationStatusRepository private val keyguardInteractor = mock<KeyguardInteractor>() private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() + private val latencyTracker = mock<LatencyTracker>() private val deviceStateManager = kosmos.deviceStateManager private val closedDeviceState = kosmos.foldedDeviceStateList.first() @@ -142,6 +151,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) } @@ -195,6 +205,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) displaySwitchLatencyTracker.start() @@ -370,6 +381,256 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { } } + @Test + fun unfoldingDevice_startsLatencyTracking() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + + verify(latencyTracker).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldingDevice_doesntTrackLatency() { + testScope.runTest { + setDeviceState(UNFOLDED) + displaySwitchLatencyTracker.start() + runCurrent() + + startFolding() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldedState_doesntStartTrackingOnScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_endsLatencyTrackingWhenTransitionStarts() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenScreenOn() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_doesntEndLatencyTrackingWhenScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenDeviceGoesToSleep() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_POWER_BUTTON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingWhenNewDeviceStateEmitted() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_startsOneTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_inCoolDownPeriod_trackingDisabled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_coolDownPassed_trackingWorksAsUsual() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.plus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(2)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByStartEvents() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByAnyEndEvent() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + advanceTimeBy(COOL_DOWN_DURATION - 10.milliseconds) + powerInteractor.setScreenPowerState(SCREEN_ON) + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchTimedOut_trackingCancelled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds) + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) { + setDeviceState(FOLDED) + tracker.start() + runCurrent() + } + + private suspend fun TestScope.startUnfolding() { + setDeviceState(HALF_FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private suspend fun TestScope.startFolding() { + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private fun TestScope.finishFolding() { + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + } + + private fun TestScope.finishUnfolding() { + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + } + private suspend fun setDeviceState(state: DeviceState) { foldStateRepository.emit(state) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index 75f3386ed695..b8e19248b2de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -47,6 +47,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.keyguard.TestScopeProvider; +import com.android.settingslib.volume.MediaSessions; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCaseExtKt; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -268,13 +269,15 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { @Test public void testOnRemoteVolumeChanged_newStream_noNullPointer() { MediaSession.Token token = new MediaSession.Token(Process.myUid(), null); - mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(token, 0); + var sessionId = MediaSessions.SessionId.Companion.from(token); + mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(sessionId, 0); } @Test public void testOnRemoteRemove_newStream_noNullPointer() { MediaSession.Token token = new MediaSession.Token(Process.myUid(), null); - mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(token); + var sessionId = MediaSessions.SessionId.Companion.from(token); + mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(sessionId); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt index 61ee5e04afd9..390518f3e2e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt @@ -16,8 +16,10 @@ package com.android.systemui.window.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope @@ -32,6 +34,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) class WindowRootViewModelTest : SysuiTestCase() { val kosmos = testKosmos() val testScope = kosmos.testScope diff --git a/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml new file mode 100644 index 000000000000..1ba637f379c1 --- /dev/null +++ b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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. + --> +<inset + xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/clipboard_minimized_background" + android:inset="4dp"/>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml index 91cd019c85d1..43808f215a81 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml @@ -149,9 +149,9 @@ style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="24dp" android:layout_marginHorizontal="24dp" - android:accessibilityLiveRegion="assertive" + android:layout_marginTop="24dp" + android:accessibilityLiveRegion="polite" android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 448b3e7d5ea0..915563b1ae20 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -171,12 +171,12 @@ android:layout_height="wrap_content" android:visibility="gone" android:elevation="7dp" - android:padding="8dp" + android:padding="12dp" app:layout_constraintBottom_toTopOf="@id/indication_container" app:layout_constraintStart_toStartOf="parent" - android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" - android:background="@drawable/clipboard_minimized_background"> + android:layout_marginStart="4dp" + android:layout_marginBottom="2dp" + android:background="@drawable/clipboard_minimized_background_inset"> <ImageView android:src="@drawable/ic_content_paste" android:tint="?attr/overlayButtonTextColor" diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 67f620f6fc54..8ad99abccdfe 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -16,7 +16,7 @@ <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/volume_dialog_root" + android:id="@+id/volume_dialog" android:layout_width="match_parent" android:layout_height="match_parent" android:alpha="0" diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml index 6748cfa05c35..4e3c8cc4413b 100644 --- a/packages/SystemUI/res/layout/volume_ringer_button.xml +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -13,20 +13,13 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<ImageButton xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" > - - <ImageButton - android:id="@+id/volume_drawer_button" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" - android:contentDescription="@string/volume_ringer_mode" - android:gravity="center" - android:tint="@androidprv:color/materialColorOnSurface" - android:src="@drawable/volume_ringer_item_bg" - android:background="@drawable/volume_ringer_item_bg"/> - -</FrameLayout> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/volume_ringer_item_bg" + android:contentDescription="@string/volume_ringer_mode" + android:gravity="center" + android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" + android:src="@drawable/volume_ringer_item_bg" + android:tint="@androidprv:color/materialColorOnSurface" /> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 86292039d93d..d18a90a17abe 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1351,10 +1351,6 @@ <string name="accessibility_action_label_shrink_widget">Decrease height</string> <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] --> <string name="accessibility_action_label_expand_widget">Increase height</string> - <!-- Label for accessibility action to show the next media player. [CHAR LIMIT=NONE] --> - <string name="accessibility_action_label_umo_show_next">Show next</string> - <!-- Label for accessibility action to show the previous media player. [CHAR LIMIT=NONE] --> - <string name="accessibility_action_label_umo_show_previous">Show previous</string> <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_title">Lock screen widgets</string> <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 5ef4d4014ba6..7f2c89346423 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -258,7 +258,7 @@ <style name="TextAppearance.AuthNonBioCredential.Title"> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> <item name="android:layout_marginTop">24dp</item> - <item name="android:textSize">36dp</item> + <item name="android:textSize">36sp</item> <item name="android:focusable">true</item> <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> </style> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS index 1ed8c068f974..5a59b7aaef56 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. include /core/java/android/view/accessibility/OWNERS jonesriley@google.com
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS index b65d29c6a0bb..429b4b0fccab 100644 --- a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS @@ -5,5 +5,4 @@ linyuh@google.com pauldpong@google.com praveenj@google.com vicliang@google.com -mfolkerts@google.com yuklimko@google.com diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt index 6cd763a9d3d0..bbf9a19012a4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt @@ -31,6 +31,7 @@ import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieComposition import com.airbnb.lottie.LottieProperty import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.KeyguardPINView import com.android.systemui.CoreStartable import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor @@ -50,7 +51,6 @@ import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine -import com.android.app.tracing.coroutines.launchTraced as launch /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */ @SysUISingleton @@ -65,51 +65,53 @@ constructor( private val layoutInflater: Lazy<LayoutInflater>, private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>, private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>, - private val windowManager: Lazy<WindowManager> + private val windowManager: Lazy<WindowManager>, ) : CoreStartable { override fun start() { - applicationScope - .launch { - sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable -> - if (isSfpsAvailable) { - combine( - biometricStatusInteractor.get().sfpsAuthenticationReason, - deviceEntrySideFpsOverlayInteractor - .get() - .showIndicatorForDeviceEntry, - sideFpsProgressBarViewModel.get().isVisible, - ::Triple + applicationScope.launch { + sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable -> + if (isSfpsAvailable) { + combine( + biometricStatusInteractor.get().sfpsAuthenticationReason, + deviceEntrySideFpsOverlayInteractor.get().showIndicatorForDeviceEntry, + sideFpsProgressBarViewModel.get().isVisible, + ::Triple, + ) + .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair) + .collect { (combinedFlows, isInRearDisplayMode: Boolean) -> + val ( + systemServerAuthReason, + showIndicatorForDeviceEntry, + progressBarIsVisible) = + combinedFlows + Log.d( + TAG, + "systemServerAuthReason = $systemServerAuthReason, " + + "showIndicatorForDeviceEntry = " + + "$showIndicatorForDeviceEntry, " + + "progressBarIsVisible = $progressBarIsVisible", ) - .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair) - .collect { (combinedFlows, isInRearDisplayMode: Boolean) -> - val ( - systemServerAuthReason, - showIndicatorForDeviceEntry, - progressBarIsVisible) = - combinedFlows - Log.d( - TAG, - "systemServerAuthReason = $systemServerAuthReason, " + - "showIndicatorForDeviceEntry = " + - "$showIndicatorForDeviceEntry, " + - "progressBarIsVisible = $progressBarIsVisible" - ) - if (!isInRearDisplayMode) { - if (progressBarIsVisible) { - hide() - } else if (systemServerAuthReason != NotRunning) { - show() - } else if (showIndicatorForDeviceEntry) { - show() - } else { - hide() - } + if (!isInRearDisplayMode) { + if (progressBarIsVisible) { + hide() + } else if (systemServerAuthReason != NotRunning) { + show() + } else if (showIndicatorForDeviceEntry) { + show() + overlayView?.announceForAccessibility( + applicationContext.resources.getString( + R.string.accessibility_side_fingerprint_indicator_label + ) + ) + } else { + hide() } } - } + } } } + } } private var overlayView: View? = null @@ -119,7 +121,7 @@ constructor( if (overlayView?.isAttachedToWindow == true) { Log.d( TAG, - "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request" + "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request", ) return } @@ -137,11 +139,6 @@ constructor( overlayView!!.visibility = View.INVISIBLE Log.d(TAG, "show(): adding overlayView $overlayView") windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams) - overlayView!!.announceForAccessibility( - applicationContext.resources.getString( - R.string.accessibility_side_fingerprint_indicator_label - ) - ) } /** Hide the side fingerprint sensor indicator */ @@ -163,7 +160,7 @@ constructor( fun bind( overlayView: View, viewModel: SideFpsOverlayViewModel, - windowManager: WindowManager + windowManager: WindowManager, ) { overlayView.repeatWhenAttached { val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation) @@ -186,7 +183,7 @@ constructor( object : View.AccessibilityDelegate() { override fun dispatchPopulateAccessibilityEvent( host: View, - event: AccessibilityEvent + event: AccessibilityEvent, ): Boolean { return if ( event.getEventType() === diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java index 2e1b5ad177b5..e456310febfd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java +++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java @@ -17,16 +17,19 @@ package com.android.systemui.communal; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; -import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.shared.model.DozeStateModel; import com.android.systemui.shared.condition.Condition; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.kotlin.JavaAdapter; import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.Job; import javax.inject.Inject; @@ -38,6 +41,10 @@ public class DeviceInactiveCondition extends Condition { private final KeyguardStateController mKeyguardStateController; private final WakefulnessLifecycle mWakefulnessLifecycle; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final KeyguardInteractor mKeyguardInteractor; + private final JavaAdapter mJavaAdapter; + private Job mAnyDozeListenerJob; + private boolean mAnyDoze; private final KeyguardStateController.Callback mKeyguardStateCallback = new KeyguardStateController.Callback() { @Override @@ -63,12 +70,14 @@ public class DeviceInactiveCondition extends Condition { @Inject public DeviceInactiveCondition(@Application CoroutineScope scope, KeyguardStateController keyguardStateController, - WakefulnessLifecycle wakefulnessLifecycle, - KeyguardUpdateMonitor keyguardUpdateMonitor) { + WakefulnessLifecycle wakefulnessLifecycle, KeyguardUpdateMonitor keyguardUpdateMonitor, + KeyguardInteractor keyguardInteractor, JavaAdapter javaAdapter) { super(scope); mKeyguardStateController = keyguardStateController; mWakefulnessLifecycle = wakefulnessLifecycle; mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mKeyguardInteractor = keyguardInteractor; + mJavaAdapter = javaAdapter; } @Override @@ -77,6 +86,11 @@ public class DeviceInactiveCondition extends Condition { mKeyguardStateController.addCallback(mKeyguardStateCallback); mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); + mAnyDozeListenerJob = mJavaAdapter.alwaysCollectFlow( + mKeyguardInteractor.getDozeTransitionModel(), dozeModel -> { + mAnyDoze = !DozeStateModel.Companion.isDozeOff(dozeModel.getTo()); + updateState(); + }); } @Override @@ -84,6 +98,7 @@ public class DeviceInactiveCondition extends Condition { mKeyguardStateController.removeCallback(mKeyguardStateCallback); mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback); mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); + mAnyDozeListenerJob.cancel(null); } @Override @@ -92,10 +107,10 @@ public class DeviceInactiveCondition extends Condition { } private void updateState() { - final boolean asleep = - mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP - || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP; - updateCondition(asleep || mKeyguardStateController.isShowing() - || mKeyguardUpdateMonitor.isDreaming()); + final boolean asleep = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP; + // Doze/AoD is also a dream, but we should never override it with low light as to the user + // it's totally unrelated. + updateCondition(!mAnyDoze && (asleep || mKeyguardStateController.isShowing() + || mKeyguardUpdateMonitor.isDreaming())); } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index a4860dfc47ce..49003a735fbd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -202,12 +202,6 @@ abstract class BaseCommunalViewModel( /** Called as the user request to show the customize widget button. */ open fun onLongClick() {} - /** Called as the user requests to switch to the previous player in UMO. */ - open fun onShowPreviousMedia() {} - - /** Called as the user requests to switch to the next player in UMO. */ - open fun onShowNextMedia() {} - /** Called as the UI determines that a new widget has been added to the grid. */ open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index dd4018a9d7b9..4bc44005d2fc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -254,14 +254,6 @@ constructor( } } - override fun onShowPreviousMedia() { - mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1) - } - - override fun onShowNextMedia() { - mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1) - } - override fun onTapWidget(componentName: ComponentName, rank: Int) { metricsLogger.logTapWidget(componentName.flattenToString(), rank) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index fcc3ea9f7d58..fed77090c477 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.LatencyTester import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification @@ -60,6 +61,7 @@ import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController import com.android.systemui.unfold.DisplaySwitchLatencyTracker +import com.android.systemui.unfold.NoCooldownDisplaySwitchLatencyTracker import com.android.systemui.usb.StorageNotification import com.android.systemui.util.NotificationChannels import com.android.systemui.util.StartBinderLoggerModule @@ -67,8 +69,10 @@ import com.android.systemui.wallpapers.dagger.WallpaperModule import com.android.systemui.wmshell.WMShell import dagger.Binds import dagger.Module +import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import javax.inject.Provider /** * DEPRECATED: DO NOT ADD THINGS TO THIS FILE. @@ -148,12 +152,6 @@ abstract class SystemUICoreStartableModule { @ClassKey(LatencyTester::class) abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable - /** Inject into DisplaySwitchLatencyTracker. */ - @Binds - @IntoMap - @ClassKey(DisplaySwitchLatencyTracker::class) - abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable - /** Inject into NotificationChannels. */ @Binds @IntoMap @@ -353,4 +351,15 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(ComplicationTypesUpdater::class) abstract fun bindComplicationTypesUpdater(updater: ComplicationTypesUpdater): CoreStartable + + companion object { + @Provides + @IntoMap + @ClassKey(DisplaySwitchLatencyTracker::class) + fun provideDisplaySwitchLatencyTracker( + noCoolDownVariant: Provider<NoCooldownDisplaySwitchLatencyTracker>, + coolDownVariant: Provider<DisplaySwitchLatencyTracker>, + ): CoreStartable = + if (unfoldLatencyTrackingFix()) coolDownVariant.get() else noCoolDownVariant.get() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index f85a23c1f091..eb96c921c181 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -24,6 +24,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -57,6 +58,7 @@ constructor( keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, private val communalSceneInteractor: CommunalSceneInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryInteractor: DeviceEntryInteractor, @@ -116,6 +118,17 @@ constructor( } } + @SuppressLint("MissingPermission") + private fun shouldTransitionToCommunal( + shouldShowCommunal: Boolean, + isCommunalAvailable: Boolean, + ) = + if (communalSettingsInteractor.isV2FlagEnabled()) { + shouldShowCommunal + } else { + isCommunalAvailable && dreamManager.canStartDreaming(false) + } + @OptIn(FlowPreview::class) @SuppressLint("MissingPermission") private fun listenForDozingToDreaming() { @@ -141,9 +154,10 @@ constructor( .filterRelevantKeyguardStateAnd { isAwake -> isAwake } .sample( communalInteractor.isCommunalAvailable, + communalInteractor.shouldShowCommunal, communalSceneInteractor.isIdleOnCommunal, ) - .collect { (_, isCommunalAvailable, isIdleOnCommunal) -> + .collect { (_, isCommunalAvailable, shouldShowCommunal, isIdleOnCommunal) -> val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value @@ -177,11 +191,9 @@ constructor( if (!SceneContainerFlag.isEnabled) { startTransitionTo(KeyguardState.GLANCEABLE_HUB) } - } else if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { - // Using false for isScreenOn as canStartDreaming returns false if any - // dream, including doze, is active. - // This case handles tapping the power button to transition through - // dream -> off -> hub. + } else if ( + shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } @@ -203,6 +215,7 @@ constructor( powerInteractor.detailedWakefulness .filterRelevantKeyguardStateAnd { it.isAwake() } .sample( + communalInteractor.shouldShowCommunal, communalInteractor.isCommunalAvailable, communalSceneInteractor.isIdleOnCommunal, keyguardInteractor.biometricUnlockState, @@ -212,6 +225,7 @@ constructor( .collect { ( _, + shouldShowCommunal, isCommunalAvailable, isIdleOnCommunal, biometricUnlockState, @@ -245,7 +259,9 @@ constructor( ownerReason = "waking from dozing", ) } - } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) { + } else if ( + shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 251af11f7fe6..c1c509b8fd57 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -129,20 +129,37 @@ constructor( if (!communalSettingsInteractor.isCommunalFlagEnabled()) return if (SceneContainerFlag.isEnabled) return scope.launch { - powerInteractor.isAwake - .debounce(50L) - .filterRelevantKeyguardStateAnd { isAwake -> isAwake } - .sample(communalInteractor.isCommunalAvailable) - .collect { isCommunalAvailable -> - if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { - // This case handles tapping the power button to transition through - // dream -> off -> hub. - communalSceneInteractor.snapToScene( - newScene = CommunalScenes.Communal, - loggingReason = "from dreaming to hub", - ) + if (communalSettingsInteractor.isV2FlagEnabled()) { + powerInteractor.isAwake + .debounce(50L) + .filterRelevantKeyguardStateAnd { isAwake -> isAwake } + .sample(communalInteractor.shouldShowCommunal) + .collect { shouldShowCommunal -> + if (shouldShowCommunal) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + communalSceneInteractor.snapToScene( + newScene = CommunalScenes.Communal, + loggingReason = "from dreaming to hub", + ) + } } - } + } else { + powerInteractor.isAwake + .debounce(50L) + .filterRelevantKeyguardStateAnd { isAwake -> isAwake } + .sample(communalInteractor.isCommunalAvailable) + .collect { isCommunalAvailable -> + if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + communalSceneInteractor.snapToScene( + newScene = CommunalScenes.Communal, + loggingReason = "from dreaming to hub", + ) + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt index 382436cf9397..5f821022d580 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -215,6 +215,7 @@ constructor( animator = null, modeOnCanceled = TransitionModeOnCanceled.RESET, ) + repository.nextLockscreenTargetState.value = DEFAULT_STATE startTransition(newTransition) } diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java index 8469cb4ab565..f8072f2f79b4 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java @@ -78,7 +78,7 @@ public abstract class LowLightModule { @Provides @IntoSet - @Named(com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS) + @Named(LOW_LIGHT_PRECONDITIONS) static Condition provideLowLightCondition(LowLightCondition lowLightCondition, DirectBootCondition directBootCondition) { // Start lowlight if we are either in lowlight or in direct boot. The ordering of the diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt index 0107a5278e3e..d63c2e07b94f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt @@ -23,11 +23,11 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider -import androidx.annotation.VisibleForTesting import androidx.core.view.GestureDetectorCompat import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce import com.android.app.tracing.TraceStateLogger +import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils import com.android.systemui.Gefingerpoken import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS @@ -38,10 +38,9 @@ import com.android.systemui.res.R import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.DelayableExecutor import com.android.wm.shell.shared.animation.PhysicsAnimator -import kotlin.math.sign private const val FLING_SLOP = 1000000 -@VisibleForTesting const val DISMISS_DELAY = 100L +private const val DISMISS_DELAY = 100L private const val SCROLL_DELAY = 100L private const val RUBBERBAND_FACTOR = 0.2f private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f @@ -65,7 +64,7 @@ class MediaCarouselScrollHandler( private val closeGuts: (immediate: Boolean) -> Unit, private val falsingManager: FalsingManager, private val logSmartspaceImpression: (Boolean) -> Unit, - private val logger: MediaUiEventLogger, + private val logger: MediaUiEventLogger ) { /** Trace state logger for media carousel visibility */ private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser") @@ -97,7 +96,7 @@ class MediaCarouselScrollHandler( /** What's the currently visible player index? */ var visibleMediaIndex: Int = 0 - @VisibleForTesting set + private set /** How much are we scrolled into the current media? */ private var scrollIntoCurrentMedia: Int = 0 @@ -138,14 +137,14 @@ class MediaCarouselScrollHandler( eStart: MotionEvent?, eCurrent: MotionEvent, vX: Float, - vY: Float, + vY: Float ) = onFling(vX, vY) override fun onScroll( down: MotionEvent?, lastMotion: MotionEvent, distanceX: Float, - distanceY: Float, + distanceY: Float ) = onScroll(down!!, lastMotion, distanceX) override fun onDown(e: MotionEvent): Boolean { @@ -158,7 +157,6 @@ class MediaCarouselScrollHandler( val touchListener = object : Gefingerpoken { override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) - override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) } @@ -170,7 +168,7 @@ class MediaCarouselScrollHandler( scrollX: Int, scrollY: Int, oldScrollX: Int, - oldScrollY: Int, + oldScrollY: Int ) { if (playerWidthPlusPadding == 0) { return @@ -179,7 +177,7 @@ class MediaCarouselScrollHandler( val relativeScrollX = scrollView.relativeScrollX onMediaScrollingChanged( relativeScrollX / playerWidthPlusPadding, - relativeScrollX % playerWidthPlusPadding, + relativeScrollX % playerWidthPlusPadding ) } } @@ -211,7 +209,7 @@ class MediaCarouselScrollHandler( 0, carouselWidth, carouselHeight, - cornerRadius.toFloat(), + cornerRadius.toFloat() ) } } @@ -237,7 +235,7 @@ class MediaCarouselScrollHandler( getMaxTranslation().toFloat(), 0.0f, 1.0f, - Math.abs(contentTranslation), + Math.abs(contentTranslation) ) val settingsTranslation = (1.0f - settingsOffset) * @@ -325,7 +323,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig, + config = translationConfig ) .start() scrollView.animationTargetX = newTranslation @@ -393,7 +391,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig, + config = translationConfig ) .start() } else { @@ -432,7 +430,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = vX, - config = translationConfig, + config = translationConfig ) .start() scrollView.animationTargetX = newTranslation @@ -585,35 +583,10 @@ class MediaCarouselScrollHandler( // We need to post this to wait for the active player becomes visible. mainExecutor.executeDelayed( { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }, - SCROLL_DELAY, + SCROLL_DELAY ) } - /** - * Scrolls the media carousel by the number of players specified by [step]. If scrolling beyond - * the carousel's bounds: - * - If the carousel is not dismissible, the settings button is displayed. - * - If the carousel is dismissible, no action taken. - * - * @param step A positive number means next, and negative means previous. - */ - fun scrollByStep(step: Int) { - val destIndex = visibleMediaIndex + step - if (destIndex >= mediaContent.childCount || destIndex < 0) { - if (!showsSettingsButton) return - var translation = getMaxTranslation() * sign(-step.toFloat()) - translation = if (isRtl) -translation else translation - PhysicsAnimator.getInstance(this) - .spring(CONTENT_TRANSLATION, translation, config = translationConfig) - .start() - scrollView.animationTargetX = translation - } else if (scrollView.getContentTranslation() != 0.0f) { - resetTranslation(true) - } else { - scrollToPlayer(destIndex = destIndex) - } - } - companion object { private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") { diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt index 1b9251061f3d..9319961f5b68 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt @@ -24,7 +24,7 @@ import com.android.compose.animation.scene.UserActionResult.HideOverlay import com.android.compose.animation.scene.UserActionResult.ShowOverlay import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -38,7 +38,7 @@ class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() : mapOf( Swipe.Up to HideOverlay(Overlays.NotificationsShade), Back to HideOverlay(Overlays.NotificationsShade), - Swipe.Down(fromSource = SceneContainerEdge.TopRight) to + Swipe.Down(fromSource = SceneContainerArea.EndHalf) to ShowOverlay( Overlays.QuickSettingsShade, hideCurrentOverlays = HideCurrentOverlays.Some(Overlays.NotificationsShade), diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt index 16dff7d11002..11b014c2147f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt @@ -28,6 +28,7 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.settings.UserFileManager import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.SharedPreferencesExt.observe @@ -83,34 +84,78 @@ constructor( .flowOn(backgroundDispatcher) /** Sets for the current user the set of [TileSpec] to display as large tiles. */ - fun setLargeTilesSpecs(specs: Set<TileSpec>) { - setLargeTilesSpecsForUser(specs, userRepository.getSelectedUserInfo().id) + fun writeLargeTileSpecs(specs: Set<TileSpec>) { + with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) { + writeLargeTileSpecs(specs) + setLargeTilesDefault(false) + } } - private fun setLargeTilesSpecsForUser(specs: Set<TileSpec>, userId: Int) { - with(getSharedPrefs(userId)) { - edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply() + suspend fun deleteLargeTileDataJob() { + userRepository.selectedUserInfo.collect { userInfo -> + getSharedPrefs(userInfo.id) + .edit() + .remove(LARGE_TILES_SPECS_KEY) + .remove(LARGE_TILES_DEFAULT_KEY) + .apply() } } + private fun SharedPreferences.writeLargeTileSpecs(specs: Set<TileSpec>) { + edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply() + } + /** - * Sets the initial tiles as large, if there is no set in SharedPrefs for the [userId]. This is - * to be used when upgrading to a build that supports large/small tiles. + * Sets the initial set of large tiles. One of the following cases will happen: + * * If we are setting the default set (no value stored in settings for the list of tiles), set + * the large tiles based on [defaultLargeTilesRepository]. We do this to signal future reboots + * that we have performed the upgrade path once. In this case, we will mark that we set them + * as the default in case a restore needs to modify them later. + * * If we got a list of tiles restored from a device and nothing has modified the list of + * tiles, set all the restored tiles to large. Note that if we also restored a set of large + * tiles before this was called, [LARGE_TILES_DEFAULT_KEY] will be false and we won't + * overwrite it. + * * If we got a list of tiles from settings, we consider that we upgraded in place and then we + * will set all those tiles to large IF there's no current set of large tiles. * * Even if largeTilesSpec is read Eagerly before we know if we are in an initial state, because * we are not writing the default values to the SharedPreferences, the file will not contain the * key and this call will succeed, as long as there hasn't been any calls to setLargeTilesSpecs * for that user before. */ - fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, userId: Int) { + fun setInitialOrUpgradeLargeTiles(upgradePath: TilesUpgradePath, userId: Int) { with(getSharedPrefs(userId)) { - if (!contains(LARGE_TILES_SPECS_KEY)) { - logger.i("Setting upgraded large tiles for user $userId: $specs") - setLargeTilesSpecsForUser(specs, userId) + when (upgradePath) { + is TilesUpgradePath.DefaultSet -> { + writeLargeTileSpecs(defaultLargeTilesRepository.defaultLargeTiles) + logger.i("Large tiles set to default on init") + setLargeTilesDefault(true) + } + is TilesUpgradePath.RestoreFromBackup -> { + if ( + getBoolean(LARGE_TILES_DEFAULT_KEY, false) || + !contains(LARGE_TILES_SPECS_KEY) + ) { + writeLargeTileSpecs(upgradePath.value) + logger.i("Tiles restored from backup set to large: ${upgradePath.value}") + setLargeTilesDefault(false) + } + } + is TilesUpgradePath.ReadFromSettings -> { + if (!contains(LARGE_TILES_SPECS_KEY)) { + writeLargeTileSpecs(upgradePath.value) + logger.i("Tiles read from settings set to large: ${upgradePath.value}") + setLargeTilesDefault(false) + } + } } } } + private fun SharedPreferences.setLargeTilesDefault(value: Boolean) { + edit().putBoolean(LARGE_TILES_DEFAULT_KEY, value).apply() + } + private fun getSharedPrefs(userId: Int): SharedPreferences { return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, userId) } @@ -118,6 +163,7 @@ constructor( companion object { private const val TAG = "QSPreferencesRepository" private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" + private const val LARGE_TILES_DEFAULT_KEY = "large_tiles_default" const val FILE_NAME = "quick_settings_prefs" } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt index 86838b438bc6..9b98797ef393 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -27,10 +28,20 @@ class QSPreferencesInteractor @Inject constructor(private val repo: QSPreference val largeTilesSpecs: Flow<Set<TileSpec>> = repo.largeTilesSpecs fun setLargeTilesSpecs(specs: Set<TileSpec>) { - repo.setLargeTilesSpecs(specs) + repo.writeLargeTileSpecs(specs) } - fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, user: Int) { - repo.setInitialLargeTilesSpecs(specs, user) + /** + * This method should be called to indicate that a "new" set of tiles has been determined for a + * particular user coming from different upgrade sources. + * + * @see TilesUpgradePath for more information + */ + fun setInitialOrUpgradeLargeTilesSpecs(specs: TilesUpgradePath, user: Int) { + repo.setInitialOrUpgradeLargeTiles(specs, user) + } + + suspend fun deleteLargeTilesDataJob() { + repo.deleteLargeTileDataJob() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt index a8ac5c34d8f9..e2797356fa96 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt @@ -19,11 +19,13 @@ package com.android.systemui.qs.panels.domain.startable import com.android.app.tracing.coroutines.launchTraced import com.android.systemui.CoreStartable import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.flags.QsInCompose import com.android.systemui.qs.panels.domain.interactor.QSPreferencesInteractor import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch class QSPanelsCoreStartable @Inject @@ -33,10 +35,14 @@ constructor( @Background private val backgroundApplicationScope: CoroutineScope, ) : CoreStartable { override fun start() { - backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") { - tileSpecRepository.tilesReadFromSetting.receiveAsFlow().collect { (tiles, userId) -> - preferenceInteractor.setInitialLargeTilesSpecs(tiles, userId) + if (QsInCompose.isEnabled) { + backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") { + tileSpecRepository.tilesUpgradePath.receiveAsFlow().collect { (tiles, userId) -> + preferenceInteractor.setInitialOrUpgradeLargeTilesSpecs(tiles, userId) + } } + } else { + backgroundApplicationScope.launch { preferenceInteractor.deleteLargeTilesDataJob() } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt index 6b7dd386bb46..c50d5dad10c1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -24,6 +24,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.res.R import com.android.systemui.retail.data.repository.RetailModeRepository @@ -78,7 +79,7 @@ interface TileSpecRepository { /** Reset the current set of tiles to the default list of tiles */ suspend fun resetToDefault(userId: Int) - val tilesReadFromSetting: ReceiveChannel<Pair<Set<TileSpec>, Int>> + val tilesUpgradePath: ReceiveChannel<Pair<TilesUpgradePath, Int>> companion object { /** Position to indicate the end of the list */ @@ -112,8 +113,8 @@ constructor( .filter { it !is TileSpec.Invalid } } - private val _tilesReadFromSetting = Channel<Pair<Set<TileSpec>, Int>>(capacity = 5) - override val tilesReadFromSetting = _tilesReadFromSetting + private val _tilesUpgradePath = Channel<Pair<TilesUpgradePath, Int>>(capacity = 5) + override val tilesUpgradePath = _tilesUpgradePath private val userTileRepositories = SparseArray<UserTileSpecRepository>() @@ -122,8 +123,8 @@ constructor( val userTileRepository = userTileSpecRepositoryFactory.create(userId) userTileRepositories.put(userId, userTileRepository) applicationScope.launchTraced("TileSpecRepository.aggregateTilesPerUser") { - for (tilesFromSettings in userTileRepository.tilesReadFromSettings) { - _tilesReadFromSetting.send(tilesFromSettings to userId) + for (tileUpgrade in userTileRepository.tilesUpgradePath) { + _tilesUpgradePath.send(tileUpgrade to userId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt index 7b56cd92a081..5aa5edaa726e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt @@ -9,6 +9,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted @@ -49,8 +50,8 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) { - private val _tilesReadFromSettings = Channel<Set<TileSpec>>(capacity = 2) - val tilesReadFromSettings: ReceiveChannel<Set<TileSpec>> = _tilesReadFromSettings + private val _tilesUpgradePath = Channel<TilesUpgradePath>(capacity = 3) + val tilesUpgradePath: ReceiveChannel<TilesUpgradePath> = _tilesUpgradePath private val defaultTiles: List<TileSpec> get() = defaultTilesRepository.defaultTiles @@ -67,14 +68,23 @@ constructor( .scan(loadTilesFromSettingsAndParse(userId)) { current, change -> change .apply(current) - .also { - if (current != it) { + .also { afterRestore -> + if (current != afterRestore) { if (change is RestoreTiles) { - logger.logTilesRestoredAndReconciled(current, it, userId) + logger.logTilesRestoredAndReconciled( + current, + afterRestore, + userId, + ) } else { - logger.logProcessTileChange(change, it, userId) + logger.logProcessTileChange(change, afterRestore, userId) } } + if (change is RestoreTiles) { + _tilesUpgradePath.send( + TilesUpgradePath.RestoreFromBackup(afterRestore.toSet()) + ) + } } // Distinct preserves the order of the elements removing later // duplicates, @@ -154,7 +164,9 @@ constructor( private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> { val loadedTiles = loadTilesFromSettings(userId) if (loadedTiles.isNotEmpty()) { - _tilesReadFromSettings.send(loadedTiles.toSet()) + _tilesUpgradePath.send(TilesUpgradePath.ReadFromSettings(loadedTiles.toSet())) + } else { + _tilesUpgradePath.send(TilesUpgradePath.DefaultSet) } return parseTileSpecs(loadedTiles, userId) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt new file mode 100644 index 000000000000..98f30c22d0f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.shared + +/** Upgrade paths indicating the source of the list of QS tiles. */ +sealed interface TilesUpgradePath { + + sealed interface UpgradeWithTiles : TilesUpgradePath { + val value: Set<TileSpec> + } + + /** This indicates a set of tiles that was read from Settings on user start */ + @JvmInline value class ReadFromSettings(override val value: Set<TileSpec>) : UpgradeWithTiles + + /** This indicates a set of tiles that was restored from backup */ + @JvmInline value class RestoreFromBackup(override val value: Set<TileSpec>) : UpgradeWithTiles + + /** + * This indicates that no tiles were read from Settings on user start so the default has been + * stored. + */ + data object DefaultSet : TilesUpgradePath +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt index 5bc26f50f70f..52c4e2fac6d5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt @@ -25,7 +25,7 @@ import com.android.compose.animation.scene.UserActionResult.ShowOverlay import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -47,7 +47,7 @@ constructor(private val editModeViewModel: EditModeViewModel) : UserActionsViewM put(Back, HideOverlay(Overlays.QuickSettingsShade)) } put( - Swipe.Down(fromSource = SceneContainerEdge.TopLeft), + Swipe.Down(fromSource = SceneContainerArea.StartHalf), ShowOverlay( Overlays.NotificationsShade, hideCurrentOverlays = diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index a4949ad66109..caa61617505f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -16,7 +16,6 @@ package com.android.systemui.scene -import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.SceneContainerTransitions -import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector -import com.android.systemui.shade.domain.interactor.ShadeInteractor import dagger.Binds import dagger.Module import dagger.Provides @@ -99,15 +96,5 @@ interface KeyguardlessSceneContainerFrameworkModule { transitionsBuilder = SceneContainerTransitions(), ) } - - @Provides - fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { - return SplitEdgeDetector( - topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, - // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to - // replace this constant with dynamic window insets. - edgeSize = 40.dp, - ) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index a018283c3953..ea11d202b119 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -16,7 +16,6 @@ package com.android.systemui.scene -import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.SceneContainerTransitions -import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector -import com.android.systemui.shade.domain.interactor.ShadeInteractor import dagger.Binds import dagger.Module import dagger.Provides @@ -121,15 +118,5 @@ interface SceneContainerFrameworkModule { transitionsBuilder = SceneContainerTransitions(), ) } - - @Provides - fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { - return SplitEdgeDetector( - topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, - // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to - // replace this constant with dynamic window insets. - edgeSize = 40.dp, - ) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 7a32491c0b67..475c0794861f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -240,7 +240,13 @@ constructor( ) { val currentSceneKey = currentScene.value val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene - if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) { + if ( + !validateSceneChange( + from = currentSceneKey, + to = resolvedScene, + loggingReason = loggingReason, + ) + ) { return } @@ -249,6 +255,7 @@ constructor( logger.logSceneChanged( from = currentSceneKey, to = resolvedScene, + sceneState = sceneState, reason = loggingReason, isInstant = false, ) @@ -272,13 +279,20 @@ constructor( familyResolver.resolvedScene.value } } ?: toScene - if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) { + if ( + !validateSceneChange( + from = currentSceneKey, + to = resolvedScene, + loggingReason = loggingReason, + ) + ) { return } logger.logSceneChanged( from = currentSceneKey, to = resolvedScene, + sceneState = null, reason = loggingReason, isInstant = true, ) @@ -489,11 +503,12 @@ constructor( * Will throw a runtime exception for illegal states (for example, attempting to change to a * scene that's not part of the current scene framework configuration). * + * @param from The current scene being transitioned away from * @param to The desired destination scene to transition to * @param loggingReason The reason why the transition is requested, for logging purposes * @return `true` if the scene change is valid; `false` if it shouldn't happen */ - private fun validateSceneChange(to: SceneKey, loggingReason: String): Boolean { + private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean { check( !shadeModeInteractor.isDualShade || (to != Scenes.Shade && to != Scenes.QuickSettings) ) { @@ -503,6 +518,10 @@ constructor( "Can't change scene to ${to.debugName} in split shade mode!" } + if (from == to) { + return false + } + if (to !in repository.allContentKeys) { return false } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index 16c2ef556de8..d00585858ccb 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -45,23 +45,30 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: ) } - fun logSceneChanged(from: SceneKey, to: SceneKey, reason: String, isInstant: Boolean) { + fun logSceneChanged( + from: SceneKey, + to: SceneKey, + sceneState: Any?, + reason: String, + isInstant: Boolean, + ) { logBuffer.log( tag = TAG, level = LogLevel.INFO, messageInitializer = { - str1 = from.toString() - str2 = to.toString() - str3 = reason + str1 = "${from.debugName} → ${to.debugName}" + str2 = reason + str3 = sceneState?.toString() bool1 = isInstant }, messagePrinter = { buildString { - append("Scene changed: $str1 → $str2") + append("Scene changed: $str1") + str3?.let { append(" (sceneState=$it)") } if (isInstant) { append(" (instant)") } - append(", reason: $str3") + append(", reason: $str2") } }, ) diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt new file mode 100644 index 000000000000..ede453dbe6b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025 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.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.FixedSizeEdgeDetector +import com.android.compose.animation.scene.SwipeSource +import com.android.compose.animation.scene.SwipeSourceDetector + +/** Identifies an area of the [SceneContainer] to detect swipe gestures on. */ +sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Resolved) : + SwipeSource { + data object StartEdge : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.LeftEdge else Resolved.RightEdge + } + ) + + data object StartHalf : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.LeftHalf else Resolved.RightHalf + } + ) + + data object EndEdge : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.RightEdge else Resolved.LeftEdge + } + ) + + data object EndHalf : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.RightHalf else Resolved.LeftHalf + } + ) + + override fun resolve(layoutDirection: LayoutDirection): Resolved { + return resolveArea(layoutDirection) + } + + sealed interface Resolved : SwipeSource.Resolved { + data object LeftEdge : Resolved + + data object LeftHalf : Resolved + + data object BottomEdge : Resolved + + data object RightEdge : Resolved + + data object RightHalf : Resolved + } +} + +/** + * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], but additionally + * detects the left and right halves of the screen (besides the edges). + * + * Corner cases (literally): A vertical swipe on the top-left corner of the screen will be resolved + * to [SceneContainerArea.Resolved.LeftHalf], whereas a horizontal swipe in the same position will + * be resolved to [SceneContainerArea.Resolved.LeftEdge]. The behavior is similar on the top-right + * corner of the screen. + * + * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL) + * should subscribe to [SceneContainerArea.StartEdge] and [SceneContainerArea.EndEdge] instead. + * These will be resolved at runtime to [SceneContainerArea.Resolved.LeftEdge] and + * [SceneContainerArea.Resolved.RightEdge] appropriately. Similarly, [SceneContainerArea.StartHalf] + * and [SceneContainerArea.EndHalf] will be resolved appropriately to + * [SceneContainerArea.Resolved.LeftHalf] and [SceneContainerArea.Resolved.RightHalf]. + * + * @param edgeSize The fixed size of each edge. + */ +class SceneContainerSwipeDetector(val edgeSize: Dp) : SwipeSourceDetector { + + private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize) + + override fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): SceneContainerArea.Resolved { + val fixedEdge = fixedEdgeDetector.source(layoutSize, position, density, orientation) + return when (fixedEdge) { + Edge.Resolved.Left -> SceneContainerArea.Resolved.LeftEdge + Edge.Resolved.Bottom -> SceneContainerArea.Resolved.BottomEdge + Edge.Resolved.Right -> SceneContainerArea.Resolved.RightEdge + else -> { + // Note: This intentionally includes Edge.Resolved.Top. At the moment, we don't need + // to detect swipes on the top edge, and consider them part of the right/left half. + if (position.x < layoutSize.width * 0.5f) { + SceneContainerArea.Resolved.LeftHalf + } else { + SceneContainerArea.Resolved.RightHalf + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 233e15846450..01bcc2400933 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent import android.view.View import androidx.compose.runtime.getValue +import androidx.compose.ui.unit.dp import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.DefaultEdgeDetector @@ -64,7 +65,6 @@ constructor( private val powerInteractor: PowerInteractor, shadeModeInteractor: ShadeModeInteractor, private val remoteInputInteractor: RemoteInputInteractor, - private val splitEdgeDetector: SplitEdgeDetector, private val logger: SceneLogger, hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory, val lightRevealScrim: LightRevealScrimViewModel, @@ -89,16 +89,20 @@ constructor( val hapticsViewModel: SceneContainerHapticsViewModel = hapticsViewModelFactory.create(view) /** - * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the + * The [SwipeSourceDetector] to use for defining which areas of the screen can be defined in the * [UserAction]s for this container. */ - val edgeDetector: SwipeSourceDetector by + val swipeSourceDetector: SwipeSourceDetector by hydrator.hydratedStateOf( - traceName = "edgeDetector", + traceName = "swipeSourceDetector", initialValue = DefaultEdgeDetector, source = shadeModeInteractor.shadeMode.map { - if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector + if (it is ShadeMode.Dual) { + SceneContainerSwipeDetector(edgeSize = 40.dp) + } else { + DefaultEdgeDetector + } }, ) @@ -241,6 +245,7 @@ constructor( logger.logSceneChanged( from = fromScene, to = toScene, + sceneState = null, reason = "user interaction", isInstant = false, ) diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt deleted file mode 100644 index f88bcb57a27d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.scene.ui.viewmodel - -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection -import com.android.compose.animation.scene.Edge -import com.android.compose.animation.scene.FixedSizeEdgeDetector -import com.android.compose.animation.scene.SwipeSource -import com.android.compose.animation.scene.SwipeSourceDetector - -/** - * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into - * top-left and top-right. - */ -enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) : - SwipeSource { - TopLeft(resolveEdge = { Resolved.TopLeft }), - TopRight(resolveEdge = { Resolved.TopRight }), - TopStart( - resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight } - ), - TopEnd( - resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft } - ), - Bottom(resolveEdge = { Resolved.Bottom }), - Left(resolveEdge = { Resolved.Left }), - Right(resolveEdge = { Resolved.Right }), - Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }), - End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left }); - - override fun resolve(layoutDirection: LayoutDirection): Resolved { - return resolveEdge(layoutDirection) - } - - enum class Resolved : SwipeSource.Resolved { - TopLeft, - TopRight, - Bottom, - Left, - Right, - } -} - -/** - * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the - * top edge is split in two: top-left and top-right. The split point between the two is dynamic and - * may change during runtime. - * - * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL) - * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These - * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and - * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and - * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and - * [SceneContainerEdge.Resolved.Right]. - * - * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e., - * percentage) of screen width to consider the split point between "top-left" and "top-right" - * edges. It is called on each source detection event. - * @param edgeSize The fixed size of each edge. - */ -class SplitEdgeDetector( - val topEdgeSplitFraction: () -> Float, - val edgeSize: Dp, -) : SwipeSourceDetector { - - private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize) - - override fun source( - layoutSize: IntSize, - position: IntOffset, - density: Density, - orientation: Orientation, - ): SceneContainerEdge.Resolved? { - val fixedEdge = - fixedEdgeDetector.source( - layoutSize, - position, - density, - orientation, - ) - return when (fixedEdge) { - Edge.Resolved.Top -> { - val topEdgeSplitFraction = topEdgeSplitFraction() - require(topEdgeSplitFraction in 0f..1f) { - "topEdgeSplitFraction must return a value between 0.0 and 1.0" - } - val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction - if (isLeftSide) SceneContainerEdge.Resolved.TopLeft - else SceneContainerEdge.Resolved.TopRight - } - Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left - Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom - Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right - null -> null - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt index b155ada87efd..1f534a5c191a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt @@ -111,10 +111,7 @@ constructor( statusbarWidth: Int, ): ShadeElement { val xPercentage = motionEvent.x / statusbarWidth - val threshold = shadeInteractor.get().getTopEdgeSplitFraction() - return if (xPercentage < threshold) { - notificationElement.get() - } else qsShadeElement.get() + return if (xPercentage < 0.5f) notificationElement.get() else qsShadeElement.get() } private fun monitorDisplayRemovals(): Job { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt index 6eaedd73ea76..2b3e4b5db453 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt @@ -34,7 +34,11 @@ constructor( override fun animateCollapseQs(fullyCollapse: Boolean) { if (shadeInteractor.isQsExpanded.value) { val key = - if (fullyCollapse || shadeModeInteractor.isDualShade) { + if ( + fullyCollapse || + shadeModeInteractor.isDualShade || + shadeModeInteractor.isSplitShade + ) { SceneFamilies.Home } else { Scenes.Shade diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index c8ce316c41dd..6d68796454eb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade.domain.interactor -import androidx.annotation.FloatRange import com.android.compose.animation.scene.TransitionKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -66,16 +65,6 @@ interface ShadeInteractor : BaseShadeInteractor { * wide as the entire screen. */ val isShadeLayoutWide: StateFlow<Boolean> - - /** - * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold - * between "top-left" and "top-right" for the purposes of dual-shade invocation. - * - * Note that this fraction only determines the *split* between the absolute left and right - * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end" - * will resolve to "top-left". - */ - @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float } /** ShadeInteractor methods with implementations that differ between non-empty impls. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index b1129a94d833..77e6a833c153 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -48,8 +48,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean - override fun getTopEdgeSplitFraction(): Float = 0.5f - override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {} override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt index c6752f867183..cf3b08c041be 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt @@ -20,10 +20,11 @@ import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.UserActionResult.ShowOverlay import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea /** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */ fun singleShadeActions( @@ -66,11 +67,10 @@ fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> { /** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */ fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> { - val notifShadeUserActionResult = UserActionResult.ShowOverlay(Overlays.NotificationsShade) - val qsShadeuserActionResult = UserActionResult.ShowOverlay(Overlays.QuickSettingsShade) return arrayOf( - Swipe.Down to notifShadeUserActionResult, - Swipe.Down(fromSource = SceneContainerEdge.TopRight) to qsShadeuserActionResult, + Swipe.Down to ShowOverlay(Overlays.NotificationsShade), + Swipe.Down(fromSource = SceneContainerArea.EndHalf) to + ShowOverlay(Overlays.QuickSettingsShade), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 155049f512d8..31fdec6147f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -93,6 +93,7 @@ public class NotificationShelf extends ActivatableNotificationView { private int mPaddingBetweenElements; private int mNotGoneIndex; private boolean mHasItemsInStableShelf; + private boolean mAlignedToEnd; private int mScrollFastThreshold; private boolean mInteractive; private boolean mAnimationsEnabled = true; @@ -412,8 +413,22 @@ public class NotificationShelf extends ActivatableNotificationView { public boolean isAlignedToEnd() { if (!NotificationMinimalism.isEnabled()) { return false; + } else if (SceneContainerFlag.isEnabled()) { + return mAlignedToEnd; + } else { + return mAmbientState.getUseSplitShade(); + } + } + + /** @see #isAlignedToEnd() */ + public void setAlignedToEnd(boolean alignedToEnd) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + return; + } + if (mAlignedToEnd != alignedToEnd) { + mAlignedToEnd = alignedToEnd; + requestLayout(); } - return mAmbientState.getUseSplitShade(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 3ba0ae3b3cb6..1a30caf0150b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -214,7 +214,6 @@ constructor( if ( secondaryChip is InternalChipModel.Active && StatusBarNotifChips.isEnabled && - !StatusBarChipsModernization.isEnabled && !isScreenReasonablyLarge ) { // If we have two showing chips and we don't have a ton of room @@ -222,8 +221,10 @@ constructor( // possible so that we have the highest chance of showing both chips (as // opposed to showing the primary chip with a lot of text and completely // hiding the secondary chip). - // Also: If StatusBarChipsModernization is enabled, then we'll do the - // squishing in Compose instead. + // TODO(b/392895330): If StatusBarChipsModernization is enabled, do the + // squishing in Compose instead, and be smart about it (e.g. if we have + // room for the first chip to show text and the second chip to be icon-only, + // do that instead of always squishing both chips.) InternalMultipleOngoingActivityChipsModel( primaryChip.squish(), secondaryChip.squish(), @@ -237,24 +238,31 @@ constructor( /** Squishes the chip down to the smallest content possible. */ private fun InternalChipModel.Active.squish(): InternalChipModel.Active { - return when (model) { + return if (model.shouldSquish()) { + InternalChipModel.Active(this.type, this.model.toIconOnly()) + } else { + this + } + } + + private fun OngoingActivityChipModel.Active.shouldSquish(): Boolean { + return when (this) { // Icon-only is already maximum squished - is OngoingActivityChipModel.Active.IconOnly -> this + is OngoingActivityChipModel.Active.IconOnly, // Countdown shows just a single digit, so already maximum squished - is OngoingActivityChipModel.Active.Countdown -> this - // The other chips have icon+text, so we should hide the text + is OngoingActivityChipModel.Active.Countdown -> false + // The other chips have icon+text, so we can squish them by hiding text is OngoingActivityChipModel.Active.Timer, is OngoingActivityChipModel.Active.ShortTimeDelta, - is OngoingActivityChipModel.Active.Text -> - InternalChipModel.Active(this.type, this.model.toIconOnly()) + is OngoingActivityChipModel.Active.Text -> true } } private fun OngoingActivityChipModel.Active.toIconOnly(): OngoingActivityChipModel.Active { // If this chip doesn't have an icon, then it only has text and we should continue showing // its text. (This is theoretically impossible because - // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon, but protect - // against it just in case.) + // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon and + // [shouldSquish] returns false for that model, but protect against it just in case.) val currentIcon = icon ?: return this return OngoingActivityChipModel.Active.IconOnly( key, @@ -271,8 +279,38 @@ constructor( */ val chips: StateFlow<MultipleOngoingActivityChipsModel> = if (StatusBarChipsModernization.isEnabled) { - incomingChipBundle - .map { bundle -> rankChips(bundle) } + combine( + incomingChipBundle.map { bundle -> rankChips(bundle) }, + isScreenReasonablyLarge, + ) { rankedChips, isScreenReasonablyLarge -> + if ( + StatusBarNotifChips.isEnabled && + !isScreenReasonablyLarge && + rankedChips.active.filter { !it.isHidden }.size >= 2 + ) { + // If we have at least two showing chips and we don't have a ton of room + // (!isScreenReasonablyLarge), then we want to make both of them as small as + // possible so that we have the highest chance of showing both chips (as + // opposed to showing the first chip with a lot of text and completely + // hiding the other chips). + val squishedActiveChips = + rankedChips.active.map { + if (!it.isHidden && it.shouldSquish()) { + it.toIconOnly() + } else { + it + } + } + + MultipleOngoingActivityChipsModel( + active = squishedActiveChips, + overflow = rankedChips.overflow, + inactive = rankedChips.inactive, + ) + } else { + rankedChips + } + } .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel()) } else { MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 09cc3f23032e..9dc651ed507a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -643,6 +643,10 @@ public final class NotificationEntry extends ListEntry { return row.isMediaRow(); } + public boolean containsCustomViews() { + return getSbn().getNotification().containsCustomViews(); + } + public void resetUserExpansion() { if (row != null) row.resetUserExpansion(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt index 6491223e6e10..f9e9bee4d809 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt @@ -12,7 +12,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.util.children /** Walks view hiearchy of a given notification to estimate its memory use. */ -internal object NotificationMemoryViewWalker { +object NotificationMemoryViewWalker { private const val TAG = "NotificationMemory" @@ -26,9 +26,13 @@ internal object NotificationMemoryViewWalker { private var softwareBitmaps = 0 fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse } + fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse } + fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse } + fun addStyle(styleUse: Int) = apply { style += styleUse } + fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply { softwareBitmaps += softwareBitmapUse } @@ -67,14 +71,14 @@ internal object NotificationMemoryViewWalker { getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), getViewUsage( ViewType.PRIVATE_CONTRACTED_VIEW, - row.privateLayout?.contractedChild + row.privateLayout?.contractedChild, ), getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), getViewUsage( ViewType.PUBLIC_VIEW, row.publicLayout?.expandedChild, row.publicLayout?.contractedChild, - row.publicLayout?.headsUpChild + row.publicLayout?.headsUpChild, ), ) .filterNotNull() @@ -107,14 +111,14 @@ internal object NotificationMemoryViewWalker { row.publicLayout?.expandedChild, row.publicLayout?.contractedChild, row.publicLayout?.headsUpChild, - seenObjects = seenObjects + seenObjects = seenObjects, ) } private fun getViewUsage( type: ViewType, vararg rootViews: View?, - seenObjects: HashSet<Int> = hashSetOf() + seenObjects: HashSet<Int> = hashSetOf(), ): NotificationViewUsage? { val usageBuilder = lazy { UsageBuilder() } rootViews.forEach { rootView -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index c7e15fdb98c7..73e8246907aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -901,6 +901,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if (!satisfiesMinHeightRequirement(view, entry, resources)) { return "inflated notification does not meet minimum height requirement"; } + + if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) { + if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) { + return "inflated notification does not meet maximum memory size requirement"; + } + } + return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java new file mode 100644 index 000000000000..c55cb6725e45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; +import android.os.Build; + +/** + * Holds compat {@link ChangeId} for {@link NotificationCustomContentMemoryVerifier}. + */ +final class NotificationCustomContentCompat { + /** + * Enables memory size checking of custom views included in notifications to ensure that + * they conform to the size limit set in `config_notificationStripRemoteViewSizeBytes` + * config.xml parameter. + * Notifications exceeding the size will be rejected. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA) + public static final long CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS = 270553691L; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt new file mode 100644 index 000000000000..a3e6a5cddc94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.app.compat.CompatChanges +import android.content.Context +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.annotation.VisibleForTesting +import com.android.app.tracing.traceSection +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** Checks whether Notifications with Custom content views conform to configured memory limits. */ +object NotificationCustomContentMemoryVerifier { + + private const val NOTIFICATION_SERVICE_TAG = "NotificationService" + + /** Notifications with custom views need to conform to maximum memory consumption. */ + @JvmStatic + fun requiresImageViewMemorySizeCheck(entry: NotificationEntry): Boolean { + if (!com.android.server.notification.Flags.notificationCustomViewUriRestriction()) { + return false + } + + return entry.containsCustomViews() + } + + /** + * This walks the custom view hierarchy contained in the passed Notification view and determines + * if the total memory consumption of all image views satisfies the limit set by + * [getStripViewSizeLimit]. It will also log to logcat if the limit exceeds + * [getWarnViewSizeLimit]. + * + * @return true if the Notification conforms to the view size limits. + */ + @JvmStatic + fun satisfiesMemoryLimits(view: View, entry: NotificationEntry): Boolean { + val mainColumnView = + view.findViewById<View>(com.android.internal.R.id.notification_main_column) + if (mainColumnView == null) { + Log.wtf( + NOTIFICATION_SERVICE_TAG, + "R.id.notification_main_column view should not be null!", + ) + return true + } + + val memorySize = + traceSection("computeViewHiearchyImageViewSize") { + computeViewHierarchyImageViewSize(view) + } + + if (memorySize > getStripViewSizeLimit(view.context)) { + val stripOversizedView = isCompatChangeEnabledForUid(entry.sbn.uid) + if (stripOversizedView) { + Log.w( + NOTIFICATION_SERVICE_TAG, + "Dropped notification due to too large RemoteViews ($memorySize bytes) on " + + "pkg: ${entry.sbn.packageName} tag: ${entry.sbn.tag} id: ${entry.sbn.id}", + ) + } else { + Log.w( + NOTIFICATION_SERVICE_TAG, + "RemoteViews too large on pkg: ${entry.sbn.packageName} " + + "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " + + "this WILL notification WILL be dropped when targetSdk " + + "is set to ${Build.VERSION_CODES.BAKLAVA}!", + ) + } + + // We still warn for size, but return "satisfies = ok" if the target SDK + // is too low. + return !stripOversizedView + } + + if (memorySize > getWarnViewSizeLimit(view.context)) { + // We emit the same warning as NotificationManagerService does to keep some consistency + // for developers. + Log.w( + NOTIFICATION_SERVICE_TAG, + "RemoteViews too large on pkg: ${entry.sbn.packageName} " + + "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " + + "this notifications might be dropped in a future release", + ) + } + return true + } + + private fun isCompatChangeEnabledForUid(uid: Int): Boolean = + try { + CompatChanges.isChangeEnabled( + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS, + uid, + ) + } catch (e: RuntimeException) { + Log.wtf(NOTIFICATION_SERVICE_TAG, "Failed to contact system_server for compat change.") + false + } + + @VisibleForTesting + @JvmStatic + fun computeViewHierarchyImageViewSize(view: View): Int = + when (view) { + is ViewGroup -> { + var use = 0 + for (i in 0 until view.childCount) { + use += computeViewHierarchyImageViewSize(view.getChildAt(i)) + } + use + } + is ImageView -> computeImageViewSize(view) + else -> 0 + } + + /** + * Returns the memory size of a Bitmap contained in a passed [ImageView] in bytes. If the view + * contains any other kind of drawable, the memory size is estimated from its intrinsic + * dimensions. + * + * @return Bitmap size in bytes or 0 if no drawable is set. + */ + private fun computeImageViewSize(view: ImageView): Int { + val drawable = view.drawable + return computeDrawableSize(drawable) + } + + private fun computeDrawableSize(drawable: Drawable?): Int { + return when (drawable) { + null -> 0 + is AdaptiveIconDrawable -> + computeDrawableSize(drawable.foreground) + + computeDrawableSize(drawable.background) + + computeDrawableSize(drawable.monochrome) + is BitmapDrawable -> drawable.bitmap.allocationByteCount + // People can sneak large drawables into those custom memory views via resources - + // we use the intrisic size as a proxy for how much memory rendering those will + // take. + else -> drawable.intrinsicWidth * drawable.intrinsicHeight * 4 + } + } + + /** @return Size of remote views after which a size warning is logged. */ + @VisibleForTesting + fun getWarnViewSizeLimit(context: Context): Int = + context.resources.getInteger( + com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes + ) + + /** @return Size of remote views after which the notification is dropped. */ + @VisibleForTesting + fun getStripViewSizeLimit(context: Context): Int = + context.resources.getInteger( + com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 20c3464536e9..589e5b8be240 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -1396,9 +1396,17 @@ constructor( */ @VisibleForTesting fun isValidView(view: View, entry: NotificationEntry, resources: Resources): String? { - return if (!satisfiesMinHeightRequirement(view, entry, resources)) { - "inflated notification does not meet minimum height requirement" - } else null + if (!satisfiesMinHeightRequirement(view, entry, resources)) { + return "inflated notification does not meet minimum height requirement" + } + + if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) { + if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) { + return "inflated notification does not meet maximum memory size requirement" + } + } + + return null } private fun satisfiesMinHeightRequirement( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt index 9fdd0bcc4ee9..0703f2de250d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt @@ -21,11 +21,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationShelf import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map /** Interactor for the [NotificationShelf] */ @SysUISingleton @@ -35,6 +38,7 @@ constructor( private val keyguardRepository: KeyguardRepository, private val deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository, private val powerInteractor: PowerInteractor, + private val shadeModeInteractor: ShadeModeInteractor, private val keyguardTransitionController: LockscreenShadeTransitionController, ) { /** Is the shelf showing on the keyguard? */ @@ -51,6 +55,16 @@ constructor( isKeyguardShowing && isBypassEnabled } + /** Should the shelf be aligned to the end in the current configuration? */ + val isAlignedToEnd: Flow<Boolean> + get() = + shadeModeInteractor.shadeMode.map { shadeMode -> + when (shadeMode) { + ShadeMode.Split -> true + else -> false + } + } + /** Transition keyguard to the locked shade, triggered by the shelf. */ fun goToLockedShadeFromShelf() { powerInteractor.wakeUpIfDozing("SHADE_CLICK", PowerManager.WAKE_REASON_GESTURE) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt index 0352a304a5c1..f663ea019319 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -16,15 +16,16 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewbinder +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.traceSection import com.android.systemui.plugins.FalsingManager +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */ object NotificationShelfViewBinder { @@ -41,6 +42,11 @@ object NotificationShelfViewBinder { viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications) } launch { viewModel.isClickable.collect(::setCanInteract) } + + if (SceneContainerFlag.isEnabled) { + launch { viewModel.isAlignedToEnd.collect(::setAlignedToEnd) } + } + registerViewListenersWhileAttached(shelf, viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt index 5ca8b53d0704..96cdda6d4a23 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt @@ -17,11 +17,13 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** ViewModel for [NotificationShelf]. */ @@ -40,6 +42,15 @@ constructor( val canModifyColorOfNotifications: Flow<Boolean> get() = interactor.isShelfStatic.map { static -> !static } + /** Is the shelf aligned to the end in the current configuration? */ + val isAlignedToEnd: Flow<Boolean> by lazy { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + interactor.isAlignedToEnd + } + } + /** Notifies that the user has clicked the shelf. */ fun onShelfClicked() { interactor.goToLockedShadeFromShelf() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 1bcc5adea6e8..54efa4a2bcf2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -478,7 +478,7 @@ constructor( /** * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out - * notifications unless in splitshade. + * notifications unless it's a large screen. */ private val alphaForShadeAndQsExpansion: Flow<Float> = if (SceneContainerFlag.isEnabled) { @@ -501,16 +501,26 @@ constructor( Split -> isAnyExpanded.filter { it }.map { 1f } Dual -> combineTransform( + shadeModeInteractor.isShadeLayoutWide, headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway, shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion, - ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion -> - if (isHeadsUpOrAnimatingAway) { + ) { + isShadeLayoutWide, + isHeadsUpOrAnimatingAway, + shadeExpansion, + qsExpansion -> + if (isShadeLayoutWide) { + if (shadeExpansion > 0f) { + emit(1f) + } + } else if (isHeadsUpOrAnimatingAway) { // Ensure HUNs will be visible in QS shade (at least while // unlocked) emit(1f) } else if (shadeExpansion > 0f || qsExpansion > 0f) { - // Fade out as QS shade expands + // On a narrow screen, the QS shade overlaps with lockscreen + // notifications. Fade them out as the QS shade expands. emit(1f - qsExpansion) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index e33baf7c33ae..ded964d8a1cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -57,11 +57,11 @@ import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; import com.android.systemui.statusbar.CommandQueue; @@ -76,11 +76,11 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; -import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.wmshell.BubblesManager; @@ -115,7 +115,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final static String TAG = "StatusBarNotificationActivityStarter"; private final Context mContext; - private final int mDisplayId; private final Handler mMainThreadHandler; private final Executor mUiBgExecutor; @@ -155,8 +154,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Inject StatusBarNotificationActivityStarter( - Context context, - @DisplayId int displayId, + @ShadeDisplayAware Context context, Handler mainThreadHandler, @Background Executor uiBgExecutor, NotificationVisibilityProvider visibilityProvider, @@ -189,7 +187,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit PowerInteractor powerInteractor, UserTracker userTracker) { mContext = context; - mDisplayId = displayId; mMainThreadHandler = mainThreadHandler; mUiBgExecutor = uiBgExecutor; mVisibilityProvider = visibilityProvider; @@ -493,6 +490,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean animate, boolean isActivityIntent) { mLogger.logStartNotificationIntent(entry); + final int displayId = mContext.getDisplayId(); try { ActivityTransitionAnimator.Controller animationController = new StatusBarTransitionAnimatorController( @@ -501,7 +499,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, isActivityIntent); mActivityTransitionAnimator.startPendingIntentWithAnimation( animationController, @@ -511,11 +509,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit long eventTime = row.getAndResetLastActionUpTime(); Bundle options = eventTime > 0 ? getActivityOptions( - mDisplayId, + displayId, adapter, mKeyguardStateController.isShowing(), eventTime) - : getActivityOptions(mDisplayId, adapter); + : getActivityOptions(displayId, adapter); int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null, null, null, options); mLogger.logSendPendingIntent(entry, intent, result); @@ -533,6 +531,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid, @NonNull ExpandableNotificationRow row) { boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); + final int displayId = mContext.getDisplayId(); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override public boolean onDismiss() { @@ -544,7 +543,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( @@ -552,7 +551,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit (adapter) -> TaskStackBuilder.create(mContext) .addNextIntentWithParentStack(intent) .startActivities(getActivityOptions( - mDisplayId, + displayId, adapter), new UserHandle(UserHandle.getUserId(appUid)))); }); @@ -571,6 +570,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startHistoryIntent(View view, boolean showHistory) { ModesEmptyShadeFix.assertInLegacyMode(); + final int displayId = mContext.getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -597,13 +597,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( animationController, animate, intent.getPackage(), (adapter) -> tsb.startActivities( - getActivityOptions(mDisplayId, adapter), + getActivityOptions(displayId, adapter), mUserTracker.getUserHandle())); }); return true; @@ -620,6 +620,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) { + final int displayId = mContext.getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -642,13 +643,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( animationController, animate, intentInfo.getTargetIntent().getPackage(), (adapter) -> tsb.startActivities( - getActivityOptions(mDisplayId, adapter), + getActivityOptions(displayId, adapter), mUserTracker.getUserHandle())); }); return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt index 72d093c65a91..9f05850f3405 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt @@ -22,11 +22,11 @@ interface SplitShadeStateController { /** Returns true if the device should use the split notification shade. */ @Deprecated( - message = "This is deprecated, please use ShadeInteractor#shadeMode instead", + message = "This is deprecated, please use ShadeModeInteractor#shadeMode instead", replaceWith = ReplaceWith( - "shadeInteractor.shadeMode", - "com.android.systemui.shade.domain.interactor.ShadeInteractor", + "shadeModeInteractor.shadeMode", + "com.android.systemui.shade.domain.interactor.ShadeModeInteractor", ), ) fun shouldUseSplitNotificationShade(resources: Resources): Boolean diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS index 0ec996be72de..9b4902a9e7b2 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS @@ -6,5 +6,4 @@ madym@google.com mgalhardo@google.com petrcermak@google.com stevenckng@google.com -tkachenkoi@google.com -vanjan@google.com
\ No newline at end of file +vanjan@google.com diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index f5aac720fd47..cd401d5deb6e 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -19,8 +19,11 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.app.tracing.TraceUtils.traceAsync import com.android.app.tracing.instantForTrack +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -30,10 +33,12 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessModel import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.util.Compile import com.android.systemui.util.Utils.isDeviceFoldable @@ -42,17 +47,23 @@ import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.race import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.measureTimeMillis -import java.time.Duration import java.util.concurrent.Executor import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout /** @@ -73,63 +84,96 @@ constructor( @Application private val applicationScope: CoroutineScope, private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, private val systemClock: SystemClock, - private val deviceStateManager: DeviceStateManager + private val deviceStateManager: DeviceStateManager, + private val latencyTracker: LatencyTracker, ) : CoreStartable { private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() private val isAodEnabled: Boolean get() = keyguardInteractor.isAodAvailable.value + private val displaySwitchStarted = + deviceStateRepository.state.pairwise().filter { + // Start tracking only when the foldable device is + // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + + private var startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow()) + + private var isCoolingDown = false + override fun start() { if (!isDeviceFoldable(context.resources, deviceStateManager)) { return } applicationScope.launch(context = backgroundDispatcher) { - deviceStateRepository.state - .pairwise() - .filter { - // Start tracking only when the foldable device is - // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or - // unfolding(FOLDED -> HALF_FOLD/UNFOLDED) - foldableDeviceState -> - foldableDeviceState.previousValue == DeviceState.FOLDED || - foldableDeviceState.newValue == DeviceState.FOLDED + displaySwitchStarted.collectLatest { (previousState, newState) -> + if (isCoolingDown) return@collectLatest + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + instantForTrack(TAG) { "unfold latency tracking started" } } - .flatMapLatest { foldableDeviceState -> - flow { - var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() - val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withBeforeFields( - foldableDeviceState.previousValue.toStatsInt() - ) - + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + val event = + DisplaySwitchLatencyEvent().withBeforeFields(previousState.toStatsInt()) val displaySwitchTimeMs = measureTimeMillis(systemClock) { - try { - withTimeout(SCREEN_EVENT_TIMEOUT) { - traceAsync(TAG, "displaySwitch") { - waitForDisplaySwitch(toFoldableDeviceState) - } - } - } catch (e: TimeoutCancellationException) { - Log.e(TAG, "Wait for display switch timed out") + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(newState.toStatsInt()) } } - - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withAfterFields( - toFoldableDeviceState, - displaySwitchTimeMs.toInt(), - getCurrentState() - ) - emit(displaySwitchLatencyEvent) + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + logDisplaySwitchEvent(event, newState, displaySwitchTimeMs) } + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { "tracking timed out" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } catch (e: CancellationException) { + instantForTrack(TAG) { "new state interrupted, entering cool down" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + startCoolDown() } - .collect { displaySwitchLatencyLogger.log(it) } + } } } + @OptIn(FlowPreview::class) + private fun startCoolDown() { + if (isCoolingDown) return + isCoolingDown = true + applicationScope.launch(context = backgroundDispatcher) { + val startTime = systemClock.elapsedRealtime() + try { + startOrEndEvent.timeout(COOL_DOWN_DURATION).collect() + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { + "cool down finished, lasted ${systemClock.elapsedRealtime() - startTime} ms" + } + isCoolingDown = false + } + } + } + + private fun logDisplaySwitchEvent( + event: DisplaySwitchLatencyEvent, + toFoldableDeviceState: DeviceState, + displaySwitchTimeMs: Long, + ) { + displaySwitchLatencyLogger.log( + event.withAfterFields( + toFoldableDeviceState.toStatsInt(), + displaySwitchTimeMs.toInt(), + getCurrentState(), + ) + ) + } + private fun DeviceState.toStatsInt(): Int = when (this) { DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED @@ -152,9 +196,20 @@ constructor( } } + private fun anyEndEventFlow(): Flow<Any> { + val unfoldStatus = + unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted } + // dropping first emission as we're only interested in new emissions, not current state + val screenOn = + powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON } + val goToSleep = + powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) } + return merge(screenOn, goToSleep, unfoldStatus) + } + private fun shouldWaitForTransitionStart( toFoldableDeviceState: Int, - isTransitionEnabled: Boolean + isTransitionEnabled: Boolean, ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled) private suspend fun waitForScreenTurnedOn() { @@ -165,12 +220,13 @@ constructor( private suspend fun waitForGoToSleepWithScreenOff() { traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { - powerInteractor.detailedWakefulness - .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } - .first() + powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first() } } + private fun sleepWithScreenOff(model: WakefulnessModel) = + model.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled + private fun getCurrentState(): Int = when { isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD @@ -205,7 +261,7 @@ constructor( private fun DisplaySwitchLatencyEvent.withAfterFields( toFoldableDeviceState: Int, displaySwitchTimeMs: Int, - toState: Int + toState: Int, ): DisplaySwitchLatencyEvent { log { "toFoldableDeviceState=$toFoldableDeviceState, " + @@ -217,7 +273,7 @@ constructor( return copy( toFoldableDeviceState = toFoldableDeviceState, latencyMs = displaySwitchTimeMs, - toState = toState + toState = toState, ) } @@ -250,14 +306,15 @@ constructor( val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN, val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN, val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN, - val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN + val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN, ) companion object { private const val VALUE_UNKNOWN = -1 private const val TAG = "DisplaySwitchLatency" private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) - private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds + @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds private const val FOLDABLE_DEVICE_STATE_UNKNOWN = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN diff --git a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt new file mode 100644 index 000000000000..6ac0bb168f18 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.unfold + +import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import android.util.Log +import com.android.app.tracing.TraceUtils.traceAsync +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.app.tracing.instantForTrack +import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor +import com.android.systemui.util.Compile +import com.android.systemui.util.Utils.isDeviceFoldable +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.kotlin.race +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.measureTimeMillis +import java.time.Duration +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withTimeout + +/** + * Old version of [DisplaySwitchLatencyTracker] tracking only [DisplaySwitchLatencyEvent]. Which + * version is used for tracking depends on [unfoldLatencyTrackingFix] flag. + */ +@SysUISingleton +class NoCooldownDisplaySwitchLatencyTracker +@Inject +constructor( + private val context: Context, + private val deviceStateRepository: DeviceStateRepository, + private val powerInteractor: PowerInteractor, + private val unfoldTransitionInteractor: UnfoldTransitionInteractor, + private val animationStatusRepository: AnimationStatusRepository, + private val keyguardInteractor: KeyguardInteractor, + @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor, + @Application private val applicationScope: CoroutineScope, + private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, + private val systemClock: SystemClock, + private val deviceStateManager: DeviceStateManager, +) : CoreStartable { + + private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() + private val isAodEnabled: Boolean + get() = keyguardInteractor.isAodAvailable.value + + override fun start() { + if (!isDeviceFoldable(context.resources, deviceStateManager)) { + return + } + applicationScope.launch(context = backgroundDispatcher) { + deviceStateRepository.state + .pairwise() + .filter { + // Start tracking only when the foldable device is + // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or + // unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + .flatMapLatest { foldableDeviceState -> + flow { + var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() + val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withBeforeFields( + foldableDeviceState.previousValue.toStatsInt() + ) + + val displaySwitchTimeMs = + measureTimeMillis(systemClock) { + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(toFoldableDeviceState) + } + } + } catch (e: TimeoutCancellationException) { + Log.e(TAG, "Wait for display switch timed out") + } + } + + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState, + displaySwitchTimeMs.toInt(), + getCurrentState(), + ) + emit(displaySwitchLatencyEvent) + } + } + .collect { displaySwitchLatencyLogger.log(it) } + } + } + + private fun DeviceState.toStatsInt(): Int = + when (this) { + DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED + DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN + DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN + DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED + else -> FOLDABLE_DEVICE_STATE_UNKNOWN + } + + private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) { + val isTransitionEnabled = + unfoldTransitionInteractor.isAvailable && + animationStatusRepository.areAnimationsEnabled().first() + if (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) { + traceAsync(TAG, "waitForTransitionStart()") { + unfoldTransitionInteractor.waitForTransitionStart() + } + } else { + race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() }) + } + } + + private fun shouldWaitForTransitionStart( + toFoldableDeviceState: Int, + isTransitionEnabled: Boolean, + ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled) + + private suspend fun waitForScreenTurnedOn() { + traceAsync(TAG, "waitForScreenTurnedOn()") { + powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + } + } + + private suspend fun waitForGoToSleepWithScreenOff() { + traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { + powerInteractor.detailedWakefulness + .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } + .first() + } + } + + private fun getCurrentState(): Int = + when { + isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + isStateScreenOff() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__SCREEN_OFF + else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN + } + + private fun isStateAod(): Boolean = (isAsleepDueToFold() && isAodEnabled) + + private fun isStateScreenOff(): Boolean = (isAsleepDueToFold() && !isAodEnabled) + + private fun isAsleepDueToFold(): Boolean { + val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value + + return (lastWakefulnessEvent.isAsleep() && + (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD)) + } + + private inline fun log(msg: () -> String) { + if (DEBUG) Log.d(TAG, msg()) + } + + private fun DisplaySwitchLatencyEvent.withBeforeFields( + fromFoldableDeviceState: Int + ): DisplaySwitchLatencyEvent { + log { "fromFoldableDeviceState=$fromFoldableDeviceState" } + instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" } + + return copy(fromFoldableDeviceState = fromFoldableDeviceState) + } + + private fun DisplaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState: Int, + displaySwitchTimeMs: Int, + toState: Int, + ): DisplaySwitchLatencyEvent { + log { + "toFoldableDeviceState=$toFoldableDeviceState, " + + "toState=$toState, " + + "latencyMs=$displaySwitchTimeMs" + } + instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" } + + return copy( + toFoldableDeviceState = toFoldableDeviceState, + latencyMs = displaySwitchTimeMs, + toState = toState, + ) + } + + companion object { + private const val VALUE_UNKNOWN = -1 + private const val TAG = "DisplaySwitchLatency" + private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) + private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + + private const val FOLDABLE_DEVICE_STATE_UNKNOWN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN + const val FOLDABLE_DEVICE_STATE_CLOSED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED + const val FOLDABLE_DEVICE_STATE_HALF_OPEN = + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED + private const val FOLDABLE_DEVICE_STATE_OPEN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED + private const val FOLDABLE_DEVICE_STATE_FLIPPED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt index f806a5c52d5a..9248cc801227 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt @@ -22,6 +22,7 @@ import android.hardware.devicestate.DeviceStateManager import android.os.Trace import android.util.Log import com.android.internal.util.LatencyTracker +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener @@ -63,7 +64,7 @@ constructor( /** Registers for relevant events only if the device is foldable. */ fun init() { - if (!isFoldable) { + if (unfoldLatencyTrackingFix() || !isFoldable) { return } deviceStateManager.registerCallback(uiBgExecutor, foldStateListener) @@ -85,7 +86,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -109,7 +110,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -161,7 +162,7 @@ constructor( Log.d( TAG, "Starting ACTION_SWITCH_DISPLAY_UNFOLD, " + - "isTransitionEnabled = $isTransitionEnabled" + "isTransitionEnabled = $isTransitionEnabled", ) } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt index 885a2b0d7305..c2f86a37c6d8 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted @@ -48,6 +49,9 @@ constructor( val isAvailable: Boolean get() = repository.isAvailable + /** Flow of latest [UnfoldTransitionStatus] changes */ + val unfoldTransitionStatus: Flow<UnfoldTransitionStatus> = repository.transitionStatus + /** * This mapping emits 1 when the device is completely unfolded and 0.0 when the device is * completely folded. diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 68bffeefb0f0..4d5477052388 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -37,8 +37,6 @@ import android.media.IAudioService; import android.media.IVolumeController; import android.media.MediaRouter2Manager; import android.media.VolumePolicy; -import android.media.session.MediaController.PlaybackInfo; -import android.media.session.MediaSession.Token; import android.net.Uri; import android.os.Handler; import android.os.HandlerExecutor; @@ -61,6 +59,7 @@ import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; import com.android.settingslib.volume.MediaSessions; +import com.android.settingslib.volume.MediaSessions.SessionId; import com.android.systemui.Dumpable; import com.android.systemui.Flags; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -1402,12 +1401,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks { - private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); + private final HashMap<SessionId, Integer> mRemoteStreams = new HashMap<>(); private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX; @Override - public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) { + public void onRemoteUpdate( + SessionId token, String name, MediaSessions.VolumeInfo volumeInfo) { addStream(token, "onRemoteUpdate"); int stream = 0; @@ -1415,14 +1415,15 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa stream = mRemoteStreams.get(token); } Slog.d(TAG, - "onRemoteUpdate: stream: " + stream + " volume: " + pi.getCurrentVolume()); + "onRemoteUpdate: stream: " + + stream + " volume: " + volumeInfo.getCurrentVolume()); boolean changed = mState.states.indexOfKey(stream) < 0; final StreamState ss = streamStateW(stream); ss.dynamic = true; ss.levelMin = 0; - ss.levelMax = pi.getMaxVolume(); - if (ss.level != pi.getCurrentVolume()) { - ss.level = pi.getCurrentVolume(); + ss.levelMax = volumeInfo.getMaxVolume(); + if (ss.level != volumeInfo.getCurrentVolume()) { + ss.level = volumeInfo.getCurrentVolume(); changed = true; } if (!Objects.equals(ss.remoteLabel, name)) { @@ -1437,11 +1438,11 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override - public void onRemoteVolumeChanged(Token token, int flags) { - addStream(token, "onRemoteVolumeChanged"); + public void onRemoteVolumeChanged(SessionId sessionId, int flags) { + addStream(sessionId, "onRemoteVolumeChanged"); int stream = 0; synchronized (mRemoteStreams) { - stream = mRemoteStreams.get(token); + stream = mRemoteStreams.get(sessionId); } final boolean showUI = shouldShowUI(flags); Slog.d(TAG, "onRemoteVolumeChanged: stream: " + stream + " showui? " + showUI); @@ -1459,7 +1460,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override - public void onRemoteRemoved(Token token) { + public void onRemoteRemoved(SessionId token) { int stream; synchronized (mRemoteStreams) { if (!mRemoteStreams.containsKey(token)) { @@ -1480,7 +1481,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } public void setStreamVolume(int stream, int level) { - final Token token = findToken(stream); + final SessionId token = findToken(stream); if (token == null) { Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); return; @@ -1488,9 +1489,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mMediaSessions.setVolume(token, level); } - private Token findToken(int stream) { + private SessionId findToken(int stream) { synchronized (mRemoteStreams) { - for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) { + for (Map.Entry<SessionId, Integer> entry : mRemoteStreams.entrySet()) { if (entry.getValue().equals(stream)) { return entry.getKey(); } @@ -1499,7 +1500,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa return null; } - private void addStream(Token token, String triggeringMethod) { + private void addStream(SessionId token, String triggeringMethod) { synchronized (mRemoteStreams) { if (!mRemoteStreams.containsKey(token)) { mRemoteStreams.put(token, mNextStream); diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index 83b7c1818341..86defff4a120 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -68,7 +68,7 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.volume_dialog) - requireViewById<View>(R.id.volume_dialog_root).repeatWhenAttached { + requireViewById<View>(R.id.volume_dialog).repeatWhenAttached { coroutineScopeTraced("[Volume]dialog") { val component = componentFactory.create(this) with(component.volumeDialogViewBinder()) { bind(this@VolumeDialog) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt index 20a74b027db5..afe3d7bf217a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt @@ -17,7 +17,9 @@ package com.android.systemui.volume.dialog.domain.interactor import android.annotation.SuppressLint +import android.provider.Settings import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository import com.android.systemui.volume.Events import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope @@ -28,8 +30,9 @@ import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityMod import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible import com.android.systemui.volume.dialog.utils.VolumeTracer import javax.inject.Inject -import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -43,8 +46,6 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -private val MAX_DIALOG_SHOW_TIME: Duration = 3.seconds - /** * Handles Volume Dialog visibility state. It might change from several sources: * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change; @@ -60,8 +61,11 @@ constructor( private val tracer: VolumeTracer, private val repository: VolumeDialogVisibilityRepository, private val controller: VolumeDialogController, + private val secureSettingsRepository: SecureSettingsRepository, ) { + private val defaultTimeout = 3.seconds + @SuppressLint("SharedFlowCreation") private val mutableDismissDialogEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1) val dialogVisibility: Flow<VolumeDialogVisibilityModel> = @@ -73,7 +77,14 @@ constructor( init { merge( mutableDismissDialogEvents.mapLatest { - delay(MAX_DIALOG_SHOW_TIME) + delay( + secureSettingsRepository + .getInt( + Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, + defaultTimeout.toInt(DurationUnit.MILLISECONDS), + ) + .milliseconds + ) VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT) }, callbacksInteractor.event, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index 3d0c7d64b2a4..92ec4f554548 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -246,16 +246,12 @@ constructor( uiModel.drawerState.currentMode != uiModel.drawerState.previousMode ) { val count = uiModel.availableButtons.size - val selectedButton = - getChildAt(count - uiModel.currentButtonIndex) - .requireViewById<ImageButton>(R.id.volume_drawer_button) + val selectedButton = getChildAt(count - uiModel.currentButtonIndex) as ImageButton val previousIndex = uiModel.availableButtons.indexOfFirst { it.ringerMode == uiModel.drawerState.previousMode } - val unselectedButton = - getChildAt(count - previousIndex) - .requireViewById<ImageButton>(R.id.volume_drawer_button) + val unselectedButton = getChildAt(count - previousIndex) as ImageButton // We only need to execute on roundness animation end and volume dialog background // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. @@ -306,7 +302,7 @@ constructor( ) { val count = uiModel.availableButtons.size uiModel.availableButtons.fastForEachIndexed { index, ringerButton -> - val view = getChildAt(count - index) + val view = getChildAt(count - index) as ImageButton val isOpen = uiModel.drawerState is RingerDrawerState.Open if (index == uiModel.currentButtonIndex) { view.bindDrawerButton( @@ -323,37 +319,37 @@ constructor( onAnimationEnd?.run() } - private fun View.bindDrawerButton( + private fun ImageButton.bindDrawerButton( buttonViewModel: RingerButtonViewModel, viewModel: VolumeDialogRingerDrawerViewModel, isOpen: Boolean, isSelected: Boolean = false, isAnimated: Boolean = false, ) { + // id = buttonViewModel.viewId + setSelected(isSelected) val ringerContentDesc = context.getString(buttonViewModel.contentDescriptionResId) - with(requireViewById<ImageButton>(R.id.volume_drawer_button)) { - setImageResource(buttonViewModel.imageResId) - contentDescription = - if (isSelected && !isOpen) { - context.getString( - R.string.volume_ringer_drawer_closed_content_description, - ringerContentDesc, - ) - } else { - ringerContentDesc - } - if (isSelected && !isAnimated) { - setBackgroundResource(R.drawable.volume_drawer_selection_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnPrimary)) - background = background.mutate() - } else if (!isAnimated) { - setBackgroundResource(R.drawable.volume_ringer_item_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnSurface)) - background = background.mutate() - } - setOnClickListener { - viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected) + setImageResource(buttonViewModel.imageResId) + contentDescription = + if (isSelected && !isOpen) { + context.getString( + R.string.volume_ringer_drawer_closed_content_description, + ringerContentDesc, + ) + } else { + ringerContentDesc } + if (isSelected && !isAnimated) { + setBackgroundResource(R.drawable.volume_drawer_selection_bg) + setColorFilter(context.getColor(internalR.color.materialColorOnPrimary)) + background = background.mutate() + } else if (!isAnimated) { + setBackgroundResource(R.drawable.volume_ringer_item_bg) + setColorFilter(context.getColor(internalR.color.materialColorOnSurface)) + background = background.mutate() + } + setOnClickListener { + viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index f2d7d956291c..7cc4bcc4e11c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -74,7 +74,7 @@ constructor( val insets: MutableStateFlow<WindowInsets> = MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. - val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) + val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog) animateVisibility(root, dialog, viewModel.dialogVisibilityModel) diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml new file mode 100644 index 000000000000..eb3ba82b043b --- /dev/null +++ b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ViewFlipper + android:id="@+id/flipper" + android:layout_width="match_parent" + android:layout_height="400dp" + android:flipInterval="1000" + /> + +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml new file mode 100644 index 000000000000..e2a00bd845cd --- /dev/null +++ b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/imageview" + android:layout_width="match_parent" + android:layout_height="400dp" />
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java new file mode 100644 index 000000000000..09fa3871f6e3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry; + +import static com.google.common.truth.Truth.assertThat; + +import android.compat.testing.PlatformCompatChangeRule; +import android.platform.test.annotations.DisableFlags; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.server.notification.Flags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NotificationCustomContentMemoryVerifierFlagDisabledTest extends SysuiTestCase { + + @Rule + public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule(); + + @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION) + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS + }) + public void requiresImageViewMemorySizeCheck_flagDisabled_returnsFalse() { + NotificationEntry entry = buildAcceptableNotificationEntry(mContext); + assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) + .isFalse(); + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java new file mode 100644 index 000000000000..1cadb3c0a909 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotification; +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry; +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildOversizedNotification; +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildWarningSizedNotification; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Notification; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.platform.test.annotations.EnableFlags; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.RemoteViews; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.server.notification.Flags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; + +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileNotFoundException; + +@SmallTest +@RunWith(AndroidJUnit4.class) +@EnableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION) +public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase { + + private static final String AUTHORITY = "notification.memory.test.authority"; + private static final Uri TEST_URI = new Uri.Builder() + .scheme("content") + .authority(AUTHORITY) + .path("path") + .build(); + + @Rule + public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule(); + + @Before + public void setUp() { + TestImageContentProvider provider = new TestImageContentProvider(mContext); + mContext.getContentResolver().addProvider(AUTHORITY, provider); + provider.onCreate(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void requiresImageViewMemorySizeCheck_customViewNotification_returnsTrue() { + NotificationEntry entry = + buildAcceptableNotificationEntry( + mContext); + assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void requiresImageViewMemorySizeCheck_plainNotification_returnsFalse() { + Notification notification = + new Notification.Builder(mContext, "ChannelId") + .setContentTitle("Just a notification") + .setContentText("Yep") + .build(); + NotificationEntry entry = new NotificationEntryBuilder().setNotification( + notification).build(); + assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) + .isFalse(); + } + + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_smallNotification_returnsTrue() { + Notification.Builder notification = + buildAcceptableNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_oversizedNotification_returnsFalse() { + Notification.Builder notification = + buildOversizedNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ).isFalse(); + } + + @Test + @DisableCompatChanges( + {NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS} + ) + public void satisfiesMemoryLimits_oversizedNotification_compatDisabled_returnsTrue() { + Notification.Builder notification = + buildOversizedNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ).isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_warningSizedNotification_returnsTrue() { + Notification.Builder notification = + buildWarningSizedNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() { + NotificationEntry entry = new NotificationEntryBuilder().build(); + View view = new FrameLayout(mContext); + assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void computeViewHierarchyImageViewSize_smallNotification_returnsSensibleValue() { + Notification.Builder notification = + buildAcceptableNotification(mContext, + TEST_URI); + // This should have a size of a single image + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.computeViewHierarchyImageViewSize( + inflatedView)) + .isGreaterThan(170000); + } + + private View inflateNotification(Notification.Builder builder) { + RemoteViews remoteViews = builder.createBigContentView(); + return remoteViews.apply(mContext, new FrameLayout(mContext)); + } + + private NotificationEntry toEntry(Notification.Builder builder) { + return new NotificationEntryBuilder().setNotification(builder.build()) + .setUid(Process.myUid()).build(); + } + + + /** This provider serves the images for inflation. */ + class TestImageContentProvider extends ContentProvider { + + TestImageContentProvider(Context context) { + ProviderInfo info = new ProviderInfo(); + info.authority = AUTHORITY; + info.exported = true; + attachInfoForTesting(context, info); + setAuthorities(AUTHORITY); + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) { + return getContext().getResources().openRawResourceFd( + NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE()) + .getParcelFileDescriptor(); + } + + @Override + public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) { + return getContext().getResources().openRawResourceFd( + NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE()); + } + + @Override + public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, + CancellationSignal signal) throws FileNotFoundException { + return openTypedAssetFile(uri, mimeTypeFilter, opts); + } + + @Override + public int delete(Uri uri, Bundle extras) { + return 0; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public String getType(Uri uri) { + return "image/png"; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values, Bundle extras) { + return super.insert(uri, values, extras); + } + + @Override + public Cursor query(Uri uri, String[] projection, Bundle queryArgs, + CancellationSignal cancellationSignal) { + return super.query(uri, projection, queryArgs, cancellationSignal); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + return null; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + } + + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt new file mode 100644 index 000000000000..ca4f24da3c08 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt @@ -0,0 +1,94 @@ +/* + * 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 + */ + +@file:JvmName("NotificationCustomContentNotificationBuilder") + +package com.android.systemui.statusbar.notification.row + +import android.app.Notification +import android.app.Notification.DecoratedCustomViewStyle +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.os.Process +import android.widget.RemoteViews +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.tests.R +import org.hamcrest.Matchers.lessThan +import org.junit.Assume.assumeThat + +public val DRAWABLE_IMAGE_RESOURCE = R.drawable.romainguy_rockaway + +fun buildAcceptableNotificationEntry(context: Context): NotificationEntry { + return NotificationEntryBuilder() + .setNotification(buildAcceptableNotification(context, null).build()) + .setUid(Process.myUid()) + .build() +} + +fun buildAcceptableNotification(context: Context, uri: Uri?): Notification.Builder = + buildNotification(context, uri, 1) + +fun buildOversizedNotification(context: Context, uri: Uri): Notification.Builder { + val numImagesForOversize = + (NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context) / + drawableSizeOnDevice(context)) + 2 + return buildNotification(context, uri, numImagesForOversize) +} + +fun buildWarningSizedNotification(context: Context, uri: Uri): Notification.Builder { + val numImagesForOversize = + (NotificationCustomContentMemoryVerifier.getWarnViewSizeLimit(context) / + drawableSizeOnDevice(context)) + 1 + // The size needs to be smaller than outright stripping size. + assumeThat( + numImagesForOversize * drawableSizeOnDevice(context), + lessThan(NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context)), + ) + return buildNotification(context, uri, numImagesForOversize) +} + +fun buildNotification(context: Context, uri: Uri?, numImages: Int): Notification.Builder { + val remoteViews = RemoteViews(context.packageName, R.layout.custom_view_flipper) + repeat(numImages) { i -> + val remoteViewFlipperImageView = + RemoteViews(context.packageName, R.layout.custom_view_flipper_image) + + if (uri == null) { + remoteViewFlipperImageView.setImageViewResource( + R.id.imageview, + R.drawable.romainguy_rockaway, + ) + } else { + val imageUri = uri.buildUpon().appendPath(i.toString()).build() + remoteViewFlipperImageView.setImageViewUri(R.id.imageview, imageUri) + } + remoteViews.addView(R.id.flipper, remoteViewFlipperImageView) + } + + return Notification.Builder(context, "ChannelId") + .setSmallIcon(android.R.drawable.ic_info) + .setStyle(DecoratedCustomViewStyle()) + .setCustomContentView(remoteViews) + .setCustomBigContentView(remoteViews) + .setContentTitle("This is a remote view!") +} + +fun drawableSizeOnDevice(context: Context): Int { + val drawable = context.resources.getDrawable(DRAWABLE_IMAGE_RESOURCE) + return (drawable as BitmapDrawable).bitmap.allocationByteCount +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 2f30b745a4a3..3190d3ae8f16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -91,11 +91,11 @@ import com.android.systemui.statusbar.notification.collection.provider.LaunchFul import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository; import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -122,7 +122,6 @@ import java.util.Optional; @TestableLooper.RunWithLooper(setAsMainLooper = true) public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { - private static final int DISPLAY_ID = 0; private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Mock @@ -233,7 +232,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mNotificationActivityStarter = new StatusBarNotificationActivityStarter( getContext(), - DISPLAY_ID, mHandler, mUiBgExecutor, mVisibilityProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt index fcdda9f13099..9da8e80283b6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.service.dream.dreamManager import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -44,5 +45,6 @@ var Kosmos.fromDozingTransitionInteractor by deviceEntryInteractor = deviceEntryInteractor, wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, dreamManager = dreamManager, + communalSettingsInteractor = communalSettingsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt index 5fc31f8b9e10..f2871149de11 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.pipeline.data.repository import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -79,9 +80,9 @@ class FakeTileSpecRepository( with(getFlow(userId)) { value = defaultTilesRepository.defaultTiles } } - override val tilesReadFromSetting: Channel<Pair<Set<TileSpec>, Int>> = Channel(capacity = 10) + override val tilesUpgradePath: Channel<Pair<TilesUpgradePath, Int>> = Channel(capacity = 10) - suspend fun sendTilesReadFromSetting(tiles: Set<TileSpec>, userId: Int) { - tilesReadFromSetting.send(tiles to userId) + suspend fun sendTilesFromUpgradePath(upgradePath: TilesUpgradePath, userId: Int) { + tilesUpgradePath.send(upgradePath to userId) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt index 5ff44e5d33c5..c5de02a7281b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt @@ -26,7 +26,7 @@ val Kosmos.minimumTilesRepository: MinimumTilesRepository by Kosmos.Fixture { fakeMinimumTilesRepository } var Kosmos.fakeDefaultTilesRepository by Kosmos.Fixture { FakeDefaultTilesRepository() } -val Kosmos.defaultTilesRepository: DefaultTilesRepository by +var Kosmos.defaultTilesRepository: DefaultTilesRepository by Kosmos.Fixture { fakeDefaultTilesRepository } val Kosmos.fakeTileSpecRepository by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 825e0143800b..f0350acd83ca 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -20,7 +20,6 @@ import com.android.systemui.scene.ui.FakeOverlay import com.android.systemui.scene.ui.composable.ConstantSceneContainerTransitionsBuilder import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel -import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor @@ -99,7 +98,6 @@ val Kosmos.sceneContainerViewModelFactory by Fixture { powerInteractor = powerInteractor, shadeModeInteractor = shadeModeInteractor, remoteInputInteractor = remoteInputInteractor, - splitEdgeDetector = splitEdgeDetector, logger = sceneLogger, hapticsViewModelFactory = sceneContainerHapticsViewModelFactory, view = view, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt deleted file mode 100644 index e0b529261c4d..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.scene.ui.viewmodel - -import androidx.compose.ui.unit.dp -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.shade.domain.interactor.shadeInteractor - -var Kosmos.splitEdgeDetector: SplitEdgeDetector by - Kosmos.Fixture { - SplitEdgeDetector( - topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, - edgeSize = 40.dp, - ) - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt index b40e1e7ab33b..6b641934bc44 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.statusbar.notification.data.model.activeNotificationModel +import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel /** * Make the repository hold [count] active notifications for testing. The keys of the notifications @@ -37,3 +38,56 @@ fun ActiveNotificationListRepository.setActiveNotifs(count: Int) { } .build() } + +/** + * Adds the given notification to the repository while *maintaining any notifications already + * present*. [notif] will be ranked highest. + */ +fun ActiveNotificationListRepository.addNotif(notif: ActiveNotificationModel) { + val currentNotifications = this.activeNotifications.value.individuals + this.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + addIndividualNotif(notif) + currentNotifications.forEach { + if (it.key != notif.key) { + addIndividualNotif(it.value) + } + } + } + .build() +} + +/** + * Adds the given notification to the repository while *maintaining any notifications already + * present*. [notifs] will be ranked higher than existing notifs. + */ +fun ActiveNotificationListRepository.addNotifs(notifs: List<ActiveNotificationModel>) { + val currentNotifications = this.activeNotifications.value.individuals + val newKeys = notifs.map { it.key } + this.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + notifs.forEach { addIndividualNotif(it) } + currentNotifications.forEach { + if (!newKeys.contains(it.key)) { + addIndividualNotif(it.value) + } + } + } + .build() +} + +fun ActiveNotificationListRepository.removeNotif(keyToRemove: String) { + val currentNotifications = this.activeNotifications.value.individuals + this.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + currentNotifications.forEach { + if (it.key != keyToRemove) { + addIndividualNotif(it.value) + } + } + } + .build() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt index 2057b849c069..c7380c91f703 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.lockscreenShadeTransitionController val Kosmos.notificationShelfInteractor by Fixture { @@ -28,6 +29,7 @@ val Kosmos.notificationShelfInteractor by Fixture { keyguardRepository = keyguardRepository, deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository, powerInteractor = powerInteractor, + shadeModeInteractor = shadeModeInteractor, keyguardTransitionController = lockscreenShadeTransitionController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt index 0d6ac4481742..d787e2c190c8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt @@ -52,7 +52,6 @@ val Kosmos.statusBarNotificationActivityStarter by Kosmos.Fixture { StatusBarNotificationActivityStarter( applicationContext, - applicationContext.displayId, fakeExecutorHandler, fakeExecutor, notificationVisibilityProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt index 7bcedcaa99d1..d09d010cba2e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -21,8 +21,9 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.addNotif +import com.android.systemui.statusbar.notification.data.repository.removeNotif import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization @@ -49,51 +50,47 @@ fun inCallModel( object OngoingCallTestHelper { /** - * Sets the call state to be no call, and does it correctly based on whether - * [StatusBarChipsModernization] is enabled or not. + * Removes any ongoing call state and removes any call notification associated with [key]. Does + * it correctly based on whether [StatusBarChipsModernization] is enabled or not. + * + * @param key the notification key associated with the call notification. */ - fun setNoCallState(kosmos: Kosmos) { + fun Kosmos.removeOngoingCallState(key: String) { if (StatusBarChipsModernization.isEnabled) { - // TODO(b/372657935): Maybe don't clear *all* notifications - kosmos.activeNotificationListRepository.activeNotifications.value = - ActiveNotificationsStore() + activeNotificationListRepository.removeNotif(key) } else { - kosmos.ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall) + ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall) } } /** - * Sets the ongoing call state correctly based on whether [StatusBarChipsModernization] is - * enabled or not. + * Sets SysUI to have an ongoing call state. Does it correctly based on whether + * [StatusBarChipsModernization] is enabled or not. + * + * @param key the notification key to be associated with the call notification */ - fun setOngoingCallState( - kosmos: Kosmos, - startTimeMs: Long = 1000L, + fun Kosmos.addOngoingCallState( key: String = "notif", + startTimeMs: Long = 1000L, statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(), promotedContent: PromotedNotificationContentModel? = null, contentIntent: PendingIntent? = null, uid: Int = DEFAULT_UID, ) { if (StatusBarChipsModernization.isEnabled) { - kosmos.activeNotificationListRepository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = key, - whenTime = startTimeMs, - callType = CallType.Ongoing, - statusBarChipIcon = statusBarChipIconView, - contentIntent = contentIntent, - promotedContent = promotedContent, - uid = uid, - ) - ) - } - .build() + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = key, + whenTime = startTimeMs, + callType = CallType.Ongoing, + statusBarChipIcon = statusBarChipIconView, + contentIntent = contentIntent, + promotedContent = promotedContent, + uid = uid, + ) + ) } else { - kosmos.ongoingCallRepository.setOngoingCallState( + ongoingCallRepository.setOngoingCallState( inCallModel( startTimeMs = startTimeMs, notificationIcon = statusBarChipIconView, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt index 0d2aa4c79753..888b7e625524 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.shared.settings.data.repository.secureSettingsRepository import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository import com.android.systemui.volume.dialog.utils.volumeTracer @@ -30,5 +31,6 @@ val Kosmos.volumeDialogVisibilityInteractor by volumeTracer, volumeDialogVisibilityRepository, volumeDialogController, + secureSettingsRepository, ) } diff --git a/services/accessibility/OWNERS b/services/accessibility/OWNERS index 4e1175034b5b..ab1e9ffe3bfe 100644 --- a/services/accessibility/OWNERS +++ b/services/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners danielnorman@google.com diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index e8dddcb537cd..529a564ea607 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -100,6 +100,13 @@ flag { } flag { + name: "enable_low_vision_generic_feedback" + namespace: "accessibility" + description: "Use generic feedback for low vision." + bug: "393981463" +} + +flag { name: "enable_low_vision_hats" namespace: "accessibility" description: "Use HaTS for low vision feedback." diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 8e448676c214..db8441d2424b 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -510,6 +510,11 @@ public class AutoclickController extends BaseEventStreamTransformation { return mMetaState; } + @VisibleForTesting + boolean getIsActiveForTesting() { + return mActive; + } + /** * Updates delay that should be used when scheduling clicks. The delay will be used only for * clicks scheduled after this point (pending click tasks are not affected). diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS new file mode 100644 index 000000000000..ff812ad7e7e6 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS @@ -0,0 +1,8 @@ +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 770744. + +juchengchou@google.com +chenjean@google.com +chihtinglo@google.com diff --git a/services/core/Android.bp b/services/core/Android.bp index f98076ab41e4..00db11e72dd9 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -292,9 +292,18 @@ java_genrule { out: ["services.core.priorityboosted.jar"], } +java_genrule_combiner { + name: "services.core.combined", + static_libs: ["services.core.priorityboosted"], + headers: ["services.core.unboosted"], +} + java_library { name: "services.core", - static_libs: ["services.core.priorityboosted"], + static_libs: select(release_flag("RELEASE_SERVICES_JAVA_GENRULE_COMBINER"), { + true: ["services.core.combined"], + default: ["services.core.priorityboosted"], + }), } java_library_host { diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 350ecab1dd5f..d2a5734f323f 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -163,6 +163,10 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.storage.AppFuseBridge; import com.android.server.storage.StorageSessionController; import com.android.server.storage.StorageSessionController.ExternalStorageServiceException; +import com.android.server.storage.WatchedVolumeInfo; +import com.android.server.utils.Watchable; +import com.android.server.utils.WatchedArrayMap; +import com.android.server.utils.Watcher; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal.ScreenObserver; @@ -452,7 +456,7 @@ class StorageManagerService extends IStorageManager.Stub private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>(); /** Map from volume ID to disk */ @GuardedBy("mLock") - private final ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>(); + private final WatchedArrayMap<String, WatchedVolumeInfo> mVolumes = new WatchedArrayMap<>(); /** Map from UUID to record */ @GuardedBy("mLock") @@ -503,9 +507,9 @@ class StorageManagerService extends IStorageManager.Stub "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/(?:data|media|obb|sandbox)/)([^/]+)(/.*)?"); - private VolumeInfo findVolumeByIdOrThrow(String id) { + private WatchedVolumeInfo findVolumeByIdOrThrow(String id) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(id); + final WatchedVolumeInfo vol = mVolumes.get(id); if (vol != null) { return vol; } @@ -516,9 +520,9 @@ class StorageManagerService extends IStorageManager.Stub private VolumeRecord findRecordForPath(String path) { synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (vol.path != null && path.startsWith(vol.path)) { - return mRecords.get(vol.fsUuid); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (vol.getFsPath() != null && path.startsWith(vol.getFsPath())) { + return mRecords.get(vol.getFsUuid()); } } } @@ -764,7 +768,7 @@ class StorageManagerService extends IStorageManager.Stub break; } case H_VOLUME_MOUNT: { - final VolumeInfo vol = (VolumeInfo) msg.obj; + final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj; if (isMountDisallowed(vol)) { Slog.i(TAG, "Ignoring mount " + vol.getId() + " due to policy"); break; @@ -774,7 +778,7 @@ class StorageManagerService extends IStorageManager.Stub break; } case H_VOLUME_UNMOUNT: { - final VolumeInfo vol = (VolumeInfo) msg.obj; + final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj; unmount(vol); break; } @@ -828,7 +832,8 @@ class StorageManagerService extends IStorageManager.Stub } case H_VOLUME_STATE_CHANGED: { final SomeArgs args = (SomeArgs) msg.obj; - onVolumeStateChangedAsync((VolumeInfo) args.arg1, args.argi1, args.argi2); + onVolumeStateChangedAsync((WatchedVolumeInfo) args.arg1, args.argi1, + args.argi2); args.recycle(); break; } @@ -892,9 +897,9 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { final int size = mVolumes.size(); for (int i = 0; i < size; i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (vol.mountUserId == userId) { - vol.mountUserId = UserHandle.USER_NULL; + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (vol.getMountUserId() == userId) { + vol.setMountUserId(UserHandle.USER_NULL); mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); } } @@ -1084,7 +1089,7 @@ class StorageManagerService extends IStorageManager.Stub VolumeInfo.TYPE_PRIVATE, null, null); internal.state = VolumeInfo.STATE_MOUNTED; internal.path = Environment.getDataDirectory().getAbsolutePath(); - mVolumes.put(internal.id, internal); + mVolumes.put(internal.id, WatchedVolumeInfo.fromVolumeInfo(internal)); } private void resetIfBootedAndConnected() { @@ -1242,7 +1247,7 @@ class StorageManagerService extends IStorageManager.Stub } } for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isVisibleForUser(userId) && vol.isMountedReadable()) { final StorageVolume userVol = vol.buildStorageVolume(mContext, userId, false); mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget(); @@ -1291,21 +1296,21 @@ class StorageManagerService extends IStorageManager.Stub } private void maybeRemountVolumes(int userId) { - List<VolumeInfo> volumesToRemount = new ArrayList<>(); + List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (!vol.isPrimary() && vol.isMountedWritable() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) { // If there's a visible secondary volume mounted, // we need to update the currentUserId and remount - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); volumesToRemount.add(vol); } } } - for (VolumeInfo vol : volumesToRemount) { + for (WatchedVolumeInfo vol : volumesToRemount) { Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol); mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); @@ -1317,12 +1322,12 @@ class StorageManagerService extends IStorageManager.Stub * trying to mount doesn't have the same mount user id as the current user being maintained by * StorageManagerService and change the mount Id. The checks are same as * {@link StorageManagerService#maybeRemountVolumes(int)} - * @param VolumeInfo object to consider for changing the mountId + * @param vol {@link WatchedVolumeInfo} object to consider for changing the mountId */ - private void updateVolumeMountIdIfRequired(VolumeInfo vol) { + private void updateVolumeMountIdIfRequired(WatchedVolumeInfo vol) { synchronized (mLock) { if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) { - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); } } } @@ -1485,20 +1490,21 @@ class StorageManagerService extends IStorageManager.Stub final DiskInfo disk = mDisks.get(diskId); final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid); vol.mountUserId = userId; - mVolumes.put(volId, vol); - onVolumeCreatedLocked(vol); + WatchedVolumeInfo watchedVol = WatchedVolumeInfo.fromVolumeInfo(vol); + mVolumes.put(volId, watchedVol); + onVolumeCreatedLocked(watchedVol); } } @Override public void onVolumeStateChanged(String volId, final int newState, final int userId) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - final int oldState = vol.state; - vol.state = newState; - final VolumeInfo vInfo = new VolumeInfo(vol); - vInfo.mountUserId = userId; + final int oldState = vol.getState(); + vol.setState(newState); + final WatchedVolumeInfo vInfo = new WatchedVolumeInfo(vol); + vInfo.setMountUserId(userId); final SomeArgs args = SomeArgs.obtain(); args.arg1 = vInfo; args.argi1 = oldState; @@ -1513,11 +1519,11 @@ class StorageManagerService extends IStorageManager.Stub public void onVolumeMetadataChanged(String volId, String fsType, String fsUuid, String fsLabel) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.fsType = fsType; - vol.fsUuid = fsUuid; - vol.fsLabel = fsLabel; + vol.setFsType(fsType); + vol.setFsUuid(fsUuid); + vol.setFsLabel(fsLabel); } } } @@ -1525,9 +1531,9 @@ class StorageManagerService extends IStorageManager.Stub @Override public void onVolumePathChanged(String volId, String path) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.path = path; + vol.setFsPath(path); } } } @@ -1535,24 +1541,24 @@ class StorageManagerService extends IStorageManager.Stub @Override public void onVolumeInternalPathChanged(String volId, String internalPath) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.internalPath = internalPath; + vol.setInternalPath(internalPath); } } } @Override public void onVolumeDestroyed(String volId) { - VolumeInfo vol = null; + WatchedVolumeInfo vol = null; synchronized (mLock) { vol = mVolumes.remove(volId); } if (vol != null) { - mStorageSessionController.onVolumeRemove(vol); + mStorageSessionController.onVolumeRemove(vol.getImmutableVolumeInfo()); try { - if (vol.type == VolumeInfo.TYPE_PRIVATE) { + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mInstaller.onPrivateVolumeRemoved(vol.getFsUuid()); } } catch (Installer.InstallerException e) { @@ -1566,7 +1572,7 @@ class StorageManagerService extends IStorageManager.Stub private void onDiskScannedLocked(DiskInfo disk) { int volumeCount = 0; for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (Objects.equals(disk.id, vol.getDiskId())) { volumeCount++; } @@ -1589,19 +1595,19 @@ class StorageManagerService extends IStorageManager.Stub } @GuardedBy("mLock") - private void onVolumeCreatedLocked(VolumeInfo vol) { + private void onVolumeCreatedLocked(WatchedVolumeInfo vol) { final ActivityManagerInternal amInternal = LocalServices.getService(ActivityManagerInternal.class); - if (vol.mountUserId >= 0 && !amInternal.isUserRunning(vol.mountUserId, 0)) { + if (vol.getMountUserId() >= 0 && !amInternal.isUserRunning(vol.getMountUserId(), 0)) { Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user " - + Integer.toString(vol.mountUserId) + " is no longer running."); + + Integer.toString(vol.getMountUserId()) + " is no longer running."); return; } - if (vol.type == VolumeInfo.TYPE_EMULATED) { + if (vol.getType() == VolumeInfo.TYPE_EMULATED) { final Context volumeUserContext = mContext.createContextAsUser( - UserHandle.of(vol.mountUserId), 0); + UserHandle.of(vol.getMountUserId()), 0); boolean isMediaSharedWithParent = (volumeUserContext != null) ? volumeUserContext.getSystemService( @@ -1611,60 +1617,60 @@ class StorageManagerService extends IStorageManager.Stub // should not be skipped even if media provider instance is not running in that user // space if (!isMediaSharedWithParent - && !mStorageSessionController.supportsExternalStorage(vol.mountUserId)) { + && !mStorageSessionController.supportsExternalStorage(vol.getMountUserId())) { Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user " - + Integer.toString(vol.mountUserId) + + Integer.toString(vol.getMountUserId()) + " does not support external storage."); return; } final StorageManager storage = mContext.getSystemService(StorageManager.class); - final VolumeInfo privateVol = storage.findPrivateForEmulated(vol); + final VolumeInfo privateVol = storage.findPrivateForEmulated(vol.getVolumeInfo()); if ((Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid) && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id)) - || Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) { + || Objects.equals(privateVol.getFsUuid(), mPrimaryStorageUuid)) { Slog.v(TAG, "Found primary storage at " + vol); - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY; - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY); + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); } - } else if (vol.type == VolumeInfo.TYPE_PUBLIC) { + } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) { // TODO: only look at first public partition if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid) - && vol.disk.isDefaultPrimary()) { + && vol.getDisk().isDefaultPrimary()) { Slog.v(TAG, "Found primary storage at " + vol); - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY; - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY); + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } // Adoptable public disks are visible to apps, since they meet // public API requirement of being in a stable location. - if (vol.disk.isAdoptable()) { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + if (vol.getDisk().isAdoptable()) { + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); - } else if (vol.type == VolumeInfo.TYPE_PRIVATE) { + } else if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); - } else if (vol.type == VolumeInfo.TYPE_STUB) { - if (vol.disk.isStubVisible()) { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + } else if (vol.getType() == VolumeInfo.TYPE_STUB) { + if (vol.getDisk().isStubVisible()) { + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } else { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ); } - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); } else { Slog.d(TAG, "Skipping automatic mounting of " + vol); } } - private boolean isBroadcastWorthy(VolumeInfo vol) { + private boolean isBroadcastWorthy(WatchedVolumeInfo vol) { switch (vol.getType()) { case VolumeInfo.TYPE_PRIVATE: case VolumeInfo.TYPE_PUBLIC: @@ -1691,8 +1697,8 @@ class StorageManagerService extends IStorageManager.Stub } @GuardedBy("mLock") - private void onVolumeStateChangedLocked(VolumeInfo vol, int newState) { - if (vol.type == VolumeInfo.TYPE_EMULATED) { + private void onVolumeStateChangedLocked(WatchedVolumeInfo vol, int newState) { + if (vol.getType() == VolumeInfo.TYPE_EMULATED) { if (newState != VolumeInfo.STATE_MOUNTED) { mFuseMountedUser.remove(vol.getMountUserId()); } else if (mVoldAppDataIsolationEnabled){ @@ -1741,7 +1747,7 @@ class StorageManagerService extends IStorageManager.Stub } } - private void onVolumeStateChangedAsync(VolumeInfo vol, int oldState, int newState) { + private void onVolumeStateChangedAsync(WatchedVolumeInfo vol, int oldState, int newState) { if (newState == VolumeInfo.STATE_MOUNTED) { // Private volumes can be unmounted and re-mounted even after a user has // been unlocked; on devices that support encryption keys tied to the filesystem, @@ -1751,7 +1757,7 @@ class StorageManagerService extends IStorageManager.Stub } catch (Exception e) { // Unusable partition, unmount. try { - mVold.unmount(vol.id); + mVold.unmount(vol.getId()); } catch (Exception ee) { Slog.wtf(TAG, ee); } @@ -1762,20 +1768,20 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { // Remember that we saw this volume so we're ready to accept user // metadata, or so we can annoy them when a private volume is ejected - if (!TextUtils.isEmpty(vol.fsUuid)) { - VolumeRecord rec = mRecords.get(vol.fsUuid); + if (!TextUtils.isEmpty(vol.getFsUuid())) { + VolumeRecord rec = mRecords.get(vol.getFsUuid()); if (rec == null) { - rec = new VolumeRecord(vol.type, vol.fsUuid); - rec.partGuid = vol.partGuid; + rec = new VolumeRecord(vol.getType(), vol.getFsUuid()); + rec.partGuid = vol.getPartGuid(); rec.createdMillis = System.currentTimeMillis(); - if (vol.type == VolumeInfo.TYPE_PRIVATE) { - rec.nickname = vol.disk.getDescription(); + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { + rec.nickname = vol.getDisk().getDescription(); } mRecords.put(rec.fsUuid, rec); } else { // Handle upgrade case where we didn't store partition GUID if (TextUtils.isEmpty(rec.partGuid)) { - rec.partGuid = vol.partGuid; + rec.partGuid = vol.getPartGuid(); } } @@ -1788,7 +1794,7 @@ class StorageManagerService extends IStorageManager.Stub // before notifying other listeners. // Intentionally called without the mLock to avoid deadlocking from the Storage Service. try { - mStorageSessionController.notifyVolumeStateChanged(vol); + mStorageSessionController.notifyVolumeStateChanged(vol.getImmutableVolumeInfo()); } catch (ExternalStorageServiceException e) { Log.e(TAG, "Failed to notify volume state changed to the Storage Service", e); } @@ -1799,9 +1805,9 @@ class StorageManagerService extends IStorageManager.Stub // processes that receive the intent unnecessarily. if (mBootCompleted && isBroadcastWorthy(vol)) { final Intent intent = new Intent(VolumeInfo.ACTION_VOLUME_STATE_CHANGED); - intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); intent.putExtra(VolumeInfo.EXTRA_VOLUME_STATE, newState); - intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.fsUuid); + intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid()); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget(); @@ -1826,8 +1832,8 @@ class StorageManagerService extends IStorageManager.Stub } } - if ((vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_STUB) - && vol.state == VolumeInfo.STATE_EJECTING) { + if ((vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_STUB) + && vol.getState() == VolumeInfo.STATE_EJECTING) { // TODO: this should eventually be handled by new ObbVolume state changes /* * Some OBBs might have been unmounted when this volume was @@ -1835,7 +1841,7 @@ class StorageManagerService extends IStorageManager.Stub * remove those from the list of mounted OBBS. */ mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage( - OBB_FLUSH_MOUNT_STATE, vol.path)); + OBB_FLUSH_MOUNT_STATE, vol.getFsPath())); } maybeLogMediaMount(vol, newState); } @@ -1860,7 +1866,7 @@ class StorageManagerService extends IStorageManager.Stub } } - private void maybeLogMediaMount(VolumeInfo vol, int newState) { + private void maybeLogMediaMount(WatchedVolumeInfo vol, int newState) { if (!SecurityLog.isLoggingEnabled()) { return; } @@ -1875,10 +1881,10 @@ class StorageManagerService extends IStorageManager.Stub if (newState == VolumeInfo.STATE_MOUNTED || newState == VolumeInfo.STATE_MOUNTED_READ_ONLY) { - SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.path, label); + SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.getFsPath(), label); } else if (newState == VolumeInfo.STATE_UNMOUNTED || newState == VolumeInfo.STATE_BAD_REMOVAL) { - SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.path, label); + SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.getFsPath(), label); } } @@ -1920,18 +1926,18 @@ class StorageManagerService extends IStorageManager.Stub /** * Decide if volume is mountable per device policies. */ - private boolean isMountDisallowed(VolumeInfo vol) { + private boolean isMountDisallowed(WatchedVolumeInfo vol) { UserManager userManager = mContext.getSystemService(UserManager.class); boolean isUsbRestricted = false; - if (vol.disk != null && vol.disk.isUsb()) { + if (vol.getDisk() != null && vol.getDisk().isUsb()) { isUsbRestricted = userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER, Binder.getCallingUserHandle()); } boolean isTypeRestricted = false; - if (vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_PRIVATE - || vol.type == VolumeInfo.TYPE_STUB) { + if (vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_PRIVATE + || vol.getType() == VolumeInfo.TYPE_STUB) { isTypeRestricted = userManager .hasUserRestriction(UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA, Binder.getCallingUserHandle()); @@ -1967,6 +1973,13 @@ class StorageManagerService extends IStorageManager.Stub mContext = context; mCallbacks = new Callbacks(FgThread.get().getLooper()); + mVolumes.registerObserver(new Watcher() { + @Override + public void onChange(Watchable what) { + // When we change the list or any volume contained in it, invalidate the cache + StorageManager.invalidateVolumeListCache(); + } + }); HandlerThread hthread = new HandlerThread(TAG); hthread.start(); mHandler = new StorageManagerServiceHandler(hthread.getLooper()); @@ -2339,7 +2352,7 @@ class StorageManagerService extends IStorageManager.Stub super.mount_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); if (isMountDisallowed(vol)) { throw new SecurityException("Mounting " + volId + " restricted by policy"); } @@ -2365,23 +2378,24 @@ class StorageManagerService extends IStorageManager.Stub } } - private void mount(VolumeInfo vol) { + private void mount(WatchedVolumeInfo vol) { try { - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.id); + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.getId()); // TODO(b/135341433): Remove cautious logging when FUSE is stable Slog.i(TAG, "Mounting volume " + vol); extendWatchdogTimeout("#mount might be slow"); - mVold.mount(vol.id, vol.mountFlags, vol.mountUserId, new IVoldMountCallback.Stub() { + mVold.mount(vol.getId(), vol.getMountFlags(), vol.getMountUserId(), + new IVoldMountCallback.Stub() { @Override public boolean onVolumeChecking(FileDescriptor fd, String path, String internalPath) { - vol.path = path; - vol.internalPath = internalPath; + vol.setFsPath(path); + vol.setInternalPath(internalPath); ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd); try { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, - "SMS.startFuseFileSystem: " + vol.id); - mStorageSessionController.onVolumeMount(pfd, vol); + "SMS.startFuseFileSystem: " + vol.getId()); + mStorageSessionController.onVolumeMount(pfd, vol.getImmutableVolumeInfo()); return true; } catch (ExternalStorageServiceException e) { Slog.e(TAG, "Failed to mount volume " + vol, e); @@ -2416,21 +2430,21 @@ class StorageManagerService extends IStorageManager.Stub super.unmount_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); unmount(vol); } - private void unmount(VolumeInfo vol) { + private void unmount(WatchedVolumeInfo vol) { try { try { - if (vol.type == VolumeInfo.TYPE_PRIVATE) { + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mInstaller.onPrivateVolumeRemoved(vol.getFsUuid()); } } catch (Installer.InstallerException e) { Slog.e(TAG, "Failed unmount mirror data", e); } - mVold.unmount(vol.id); - mStorageSessionController.onVolumeUnmount(vol); + mVold.unmount(vol.getId()); + mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo()); } catch (Exception e) { Slog.wtf(TAG, e); } @@ -2442,10 +2456,10 @@ class StorageManagerService extends IStorageManager.Stub super.format_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); - final String fsUuid = vol.fsUuid; + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); + final String fsUuid = vol.getFsUuid(); try { - mVold.format(vol.id, "auto"); + mVold.format(vol.getId(), "auto"); // After a successful format above, we should forget about any // records for the old partition, since it'll never appear again @@ -3105,7 +3119,7 @@ class StorageManagerService extends IStorageManager.Stub private void warnOnNotMounted() { synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isPrimary() && vol.isMountedWritable()) { // Cool beans, we have a mounted primary volume return; @@ -3392,8 +3406,8 @@ class StorageManagerService extends IStorageManager.Stub } } - private void prepareUserStorageIfNeeded(VolumeInfo vol) throws Exception { - if (vol.type != VolumeInfo.TYPE_PRIVATE) { + private void prepareUserStorageIfNeeded(WatchedVolumeInfo vol) throws Exception { + if (vol.getType() != VolumeInfo.TYPE_PRIVATE) { return; } @@ -3411,7 +3425,7 @@ class StorageManagerService extends IStorageManager.Stub continue; } - prepareUserStorageInternal(vol.fsUuid, user.id, flags); + prepareUserStorageInternal(vol.getFsUuid(), user.id, flags); } } @@ -3960,7 +3974,7 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { final String volId = mVolumes.keyAt(i); - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); switch (vol.getType()) { case VolumeInfo.TYPE_PUBLIC: case VolumeInfo.TYPE_STUB: @@ -4112,7 +4126,7 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { final VolumeInfo[] res = new VolumeInfo[mVolumes.size()]; for (int i = 0; i < mVolumes.size(); i++) { - res[i] = mVolumes.valueAt(i); + res[i] = mVolumes.valueAt(i).getVolumeInfo(); } return res; } @@ -4708,7 +4722,8 @@ class StorageManagerService extends IStorageManager.Stub break; } case MSG_VOLUME_STATE_CHANGED: { - callback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3); + VolumeInfo volInfo = ((WatchedVolumeInfo) args.arg1).getVolumeInfo(); + callback.onVolumeStateChanged(volInfo, args.argi2, args.argi3); break; } case MSG_VOLUME_RECORD_CHANGED: { @@ -4738,7 +4753,7 @@ class StorageManagerService extends IStorageManager.Stub obtainMessage(MSG_STORAGE_STATE_CHANGED, args).sendToTarget(); } - private void notifyVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { + private void notifyVolumeStateChanged(WatchedVolumeInfo vol, int oldState, int newState) { final SomeArgs args = SomeArgs.obtain(); args.arg1 = vol.clone(); args.argi2 = oldState; @@ -4790,8 +4805,8 @@ class StorageManagerService extends IStorageManager.Stub pw.println("Volumes:"); pw.increaseIndent(); for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) continue; + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) continue; vol.dump(pw); } pw.decreaseIndent(); @@ -5088,7 +5103,7 @@ class StorageManagerService extends IStorageManager.Stub final List<String> primaryVolumeIds = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isPrimary()) { primaryVolumeIds.add(vol.getId()); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 6cca7d16842a..cce29592d912 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -8302,8 +8302,6 @@ public final class ActiveServices { if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) { @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges); - // We store them to compare the old and new while-in-use logics to each other. - // (They're not used for any other purposes.) if (allowWiu == REASON_DENIED) { allowWiu = allowWhileInUse; } @@ -8706,6 +8704,7 @@ public final class ActiveServices { + ",duration:" + tempAllowListReason.mDuration + ",callingUid:" + tempAllowListReason.mCallingUid)) + ">" + + "; allowWiu:" + allowWhileInUse + "; targetSdkVersion:" + r.appInfo.targetSdkVersion + "; callerTargetSdkVersion:" + callerTargetSdkVersion + "; startForegroundCount:" + r.mStartForegroundCount diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 2219ecc77167..b48d0a6ed547 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -66,7 +66,6 @@ import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; import static com.android.media.audio.Flags.replaceStreamBtSco; import static com.android.media.audio.Flags.ringMyCar; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; -import static com.android.media.audio.Flags.vgsVssSyncMuteOrder; import static com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl; import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE; import static com.android.server.utils.EventLogger.Event.ALOGE; @@ -4977,9 +4976,8 @@ public class AudioService extends IAudioService.Stub + roForegroundAudioControl()); pw.println("\tandroid.media.audio.scoManagedByAudio:" + scoManagedByAudio()); - pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:" - + vgsVssSyncMuteOrder()); pw.println("\tcom.android.media.audio.absVolumeIndexFix - EOL"); + pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder - EOL"); pw.println("\tcom.android.media.audio.replaceStreamBtSco:" + replaceStreamBtSco()); pw.println("\tcom.android.media.audio.equalScoLeaVcIndexRange:" @@ -9010,22 +9008,13 @@ public class AudioService extends IAudioService.Stub synced = true; continue; } - if (vgsVssSyncMuteOrder()) { - if ((isMuted() != streamMuted) && isVssMuteBijective( - stream)) { - vss.mute(isMuted(), "VGS.applyAllVolumes#1"); - } + if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) { + vss.mute(isMuted(), "VGS.applyAllVolumes#1"); } if (indexForStream != index) { vss.setIndex(index * 10, device, caller, true /*hasModifyAudioSettings*/); } - if (!vgsVssSyncMuteOrder()) { - if ((isMuted() != streamMuted) && isVssMuteBijective( - stream)) { - vss.mute(isMuted(), "VGS.applyAllVolumes#1"); - } - } } } } @@ -15093,11 +15082,13 @@ public class AudioService extends IAudioService.Stub final String key = "additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); - long delayMillis; - try { - delayMillis = Long.parseLong(reply.substring(key.length() + 1)); - } catch (NullPointerException e) { - delayMillis = 0; + long delayMillis = 0; + if (reply.contains(key)) { + try { + delayMillis = Long.parseLong(reply.substring(key.length() + 1)); + } catch (NullPointerException e) { + delayMillis = 0; + } } return delayMillis; } @@ -15123,11 +15114,13 @@ public class AudioService extends IAudioService.Stub final String key = "max_additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); - long delayMillis; - try { - delayMillis = Long.parseLong(reply.substring(key.length() + 1)); - } catch (NullPointerException e) { - delayMillis = 0; + long delayMillis = 0; + if (reply.contains(key)) { + try { + delayMillis = Long.parseLong(reply.substring(key.length() + 1)); + } catch (NullPointerException e) { + delayMillis = 0; + } } return delayMillis; } diff --git a/services/core/java/com/android/server/backup/InputBackupHelper.java b/services/core/java/com/android/server/backup/InputBackupHelper.java new file mode 100644 index 000000000000..af9606c6e70f --- /dev/null +++ b/services/core/java/com/android/server/backup/InputBackupHelper.java @@ -0,0 +1,82 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import static com.android.server.input.InputManagerInternal.BACKUP_CATEGORY_INPUT_GESTURES; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.backup.BlobBackupHelper; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.input.InputManagerInternal; + +import java.util.HashMap; +import java.util.Map; + +public class InputBackupHelper extends BlobBackupHelper { + private static final String TAG = "InputBackupHelper"; // must be < 23 chars + + // Current version of the blob schema + private static final int BLOB_VERSION = 1; + + // Key under which the payload blob is stored + private static final String KEY_INPUT_GESTURES = "input_gestures"; + + private final @UserIdInt int mUserId; + + private final @NonNull InputManagerInternal mInputManagerInternal; + + public InputBackupHelper(int userId) { + super(BLOB_VERSION, KEY_INPUT_GESTURES); + mUserId = userId; + mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); + } + + @Override + protected byte[] getBackupPayload(String key) { + Map<Integer, byte[]> payloads; + try { + payloads = mInputManagerInternal.getBackupPayload(mUserId); + } catch (Exception exception) { + Slog.e(TAG, "Failed to get backup payload for input gestures", exception); + return null; + } + + if (KEY_INPUT_GESTURES.equals(key)) { + return payloads.getOrDefault(BACKUP_CATEGORY_INPUT_GESTURES, null); + } + + return null; + } + + @Override + protected void applyRestoredPayload(String key, byte[] payload) { + Map<Integer, byte[]> payloads = new HashMap<>(); + if (KEY_INPUT_GESTURES.equals(key)) { + payloads.put(BACKUP_CATEGORY_INPUT_GESTURES, payload); + } + + try { + mInputManagerInternal.applyBackupPayload(payloads, mUserId); + } catch (Exception exception) { + Slog.e(TAG, "Failed to apply input backup payload", exception); + } + } + +} diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index 677e0c055455..b11267ef8634 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -68,6 +68,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final String COMPANION_HELPER = "companion"; private static final String SYSTEM_GENDER_HELPER = "system_gender"; private static final String DISPLAY_HELPER = "display"; + private static final String INPUT_HELPER = "input"; // These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME // are also used in the full-backup file format, so must not change unless steps are @@ -112,7 +113,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final Set<String> sEligibleHelpersForNonSystemUser = SetUtils.union(sEligibleHelpersForProfileUser, Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER, - SHORTCUT_MANAGER_HELPER)); + SHORTCUT_MANAGER_HELPER, INPUT_HELPER)); private int mUserId = UserHandle.USER_SYSTEM; private boolean mIsProfileUser = false; @@ -149,6 +150,9 @@ public class SystemBackupAgent extends BackupAgentHelper { addHelperIfEligibleForUser(SYSTEM_GENDER_HELPER, new SystemGrammaticalGenderBackupHelper(mUserId)); addHelperIfEligibleForUser(DISPLAY_HELPER, new DisplayBackupHelper(mUserId)); + if (com.android.hardware.input.Flags.enableBackupAndRestoreForInputGestures()) { + addHelperIfEligibleForUser(INPUT_HELPER, new InputBackupHelper(mUserId)); + } } @Override diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index a1e8f08db0a6..aab2760dbc66 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -122,11 +122,6 @@ public class DisplayManagerFlags { Flags.FLAG_ALWAYS_ROTATE_DISPLAY_DEVICE, Flags::alwaysRotateDisplayDevice); - private final FlagState mRefreshRateVotingTelemetry = new FlagState( - Flags.FLAG_REFRESH_RATE_VOTING_TELEMETRY, - Flags::refreshRateVotingTelemetry - ); - private final FlagState mPixelAnisotropyCorrectionEnabled = new FlagState( Flags.FLAG_ENABLE_PIXEL_ANISOTROPY_CORRECTION, Flags::enablePixelAnisotropyCorrection @@ -403,10 +398,6 @@ public class DisplayManagerFlags { return mAlwaysRotateDisplayDevice.isEnabled(); } - public boolean isRefreshRateVotingTelemetryEnabled() { - return mRefreshRateVotingTelemetry.isEnabled(); - } - public boolean isPixelAnisotropyCorrectionInLogicalDisplayEnabled() { return mPixelAnisotropyCorrectionEnabled.isEnabled(); } @@ -626,7 +617,6 @@ public class DisplayManagerFlags { pw.println(" " + mAutoBrightnessModesFlagState); pw.println(" " + mFastHdrTransitions); pw.println(" " + mAlwaysRotateDisplayDevice); - pw.println(" " + mRefreshRateVotingTelemetry); pw.println(" " + mPixelAnisotropyCorrectionEnabled); pw.println(" " + mSensorBasedBrightnessThrottling); pw.println(" " + mIdleScreenRefreshRateTimeout); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index cc0bbde370fe..8211febade60 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -191,14 +191,6 @@ flag { } flag { - name: "refresh_rate_voting_telemetry" - namespace: "display_manager" - description: "Feature flag for enabling telemetry for refresh rate voting in DisplayManager" - bug: "310029108" - is_fixed_read_only: true -} - -flag { name: "enable_pixel_anisotropy_correction" namespace: "display_manager" description: "Feature flag for enabling display anisotropy correction through LogicalDisplay upscaling" diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 1dd4a9b93277..c37733b05fba 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -229,8 +229,7 @@ public class DisplayModeDirector { mContext = context; mHandler = new DisplayModeDirectorHandler(handler.getLooper()); mInjector = injector; - mVotesStatsReporter = injector.getVotesStatsReporter( - displayManagerFlags.isRefreshRateVotingTelemetryEnabled()); + mVotesStatsReporter = injector.getVotesStatsReporter(); mSupportedModesByDisplay = new SparseArray<>(); mAppSupportedModesByDisplay = new SparseArray<>(); mDefaultModeByDisplay = new SparseArray<>(); @@ -3141,7 +3140,7 @@ public class DisplayModeDirector { SensorManagerInternal getSensorManagerInternal(); @Nullable - VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled); + VotesStatsReporter getVotesStatsReporter(); } @VisibleForTesting @@ -3281,10 +3280,9 @@ public class DisplayModeDirector { } @Override - public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + public VotesStatsReporter getVotesStatsReporter() { // if frame rate override supported, renderRates will be ignored in mode selection - return new VotesStatsReporter(supportsFrameRateOverride(), - refreshRateVotingTelemetryEnabled); + return new VotesStatsReporter(supportsFrameRateOverride()); } private DisplayManager getDisplayManager() { diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java index 7562a525b5f6..d3d49c272338 100644 --- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java +++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java @@ -36,13 +36,11 @@ class VotesStatsReporter { private static final String TAG = "VotesStatsReporter"; private static final int REFRESH_RATE_NOT_LIMITED = 1000; private final boolean mIgnoredRenderRate; - private final boolean mFrameworkStatsLogReportingEnabled; private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1; - public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) { + VotesStatsReporter(boolean ignoreRenderRate) { mIgnoredRenderRate = ignoreRenderRate; - mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled; } void reportVoteChanged(int displayId, int priority, @Nullable Vote vote) { @@ -57,29 +55,22 @@ class VotesStatsReporter { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate); - if (mFrameworkStatsLogReportingEnabled) { - FrameworkStatsLog.write( - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, - maxRefreshRate, -1); - } + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, + maxRefreshRate, -1); } private void reportVoteRemoved(int displayId, int priority) { Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1); - if (mFrameworkStatsLogReportingEnabled) { - FrameworkStatsLog.write( - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1); - } + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1); } void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode, SparseArray<Vote> votes) { - if (!mFrameworkStatsLogReportingEnabled) { - return; - } int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1; for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) { if (priority < mLastMinPriorityReported && priority < minPriority) { diff --git a/services/core/java/com/android/server/input/InputDataStore.java b/services/core/java/com/android/server/input/InputDataStore.java index e8f21fe8fb74..834f8154240e 100644 --- a/services/core/java/com/android/server/input/InputDataStore.java +++ b/services/core/java/com/android/server/input/InputDataStore.java @@ -125,8 +125,20 @@ public final class InputDataStore { } } - @VisibleForTesting - List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded) + /** + * Parses the given input stream and returns the list of {@link InputGestureData} objects. + * This parsing happens on a best effort basis. If invalid data exists in the given payload + * it will be skipped. An example of this would be a keycode that does not exist in the + * present version of Android. If the payload is malformed, instead this will throw an + * exception and require the caller to handel this appropriately for its situation. + * + * @param stream stream of the input payload of XML data + * @param utf8Encoded whether or not the input data is UTF-8 encoded + * @return list of {@link InputGestureData} objects pulled from the payload + * @throws XmlPullParserException + * @throws IOException + */ + public List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded) throws XmlPullParserException, IOException { List<InputGestureData> inputGestureDataList = new ArrayList<>(); TypedXmlPullParser parser; @@ -153,6 +165,31 @@ public final class InputDataStore { return inputGestureDataList; } + /** + * Serializes the given list of {@link InputGestureData} objects to XML in the provided output + * stream. + * + * @param stream output stream to put serialized data. + * @param utf8Encoded whether or not to encode the serialized data in UTF-8 format. + * @param inputGestureDataList the list of {@link InputGestureData} objects to serialize. + */ + public void writeInputGestureXml(OutputStream stream, boolean utf8Encoded, + List<InputGestureData> inputGestureDataList) throws IOException { + final TypedXmlSerializer serializer; + if (utf8Encoded) { + serializer = Xml.newFastSerializer(); + serializer.setOutput(stream, StandardCharsets.UTF_8.name()); + } else { + serializer = Xml.resolveSerializer(stream); + } + + serializer.startDocument(null, true); + serializer.startTag(null, TAG_ROOT); + writeInputGestureListToXml(serializer, inputGestureDataList); + serializer.endTag(null, TAG_ROOT); + serializer.endDocument(); + } + private InputGestureData readInputGestureFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException, IllegalArgumentException { InputGestureData.Builder builder = new InputGestureData.Builder(); @@ -239,24 +276,6 @@ public final class InputDataStore { return inputGestureDataList; } - @VisibleForTesting - void writeInputGestureXml(OutputStream stream, boolean utf8Encoded, - List<InputGestureData> inputGestureDataList) throws IOException { - final TypedXmlSerializer serializer; - if (utf8Encoded) { - serializer = Xml.newFastSerializer(); - serializer.setOutput(stream, StandardCharsets.UTF_8.name()); - } else { - serializer = Xml.resolveSerializer(stream); - } - - serializer.startDocument(null, true); - serializer.startTag(null, TAG_ROOT); - writeInputGestureListToXml(serializer, inputGestureDataList); - serializer.endTag(null, TAG_ROOT); - serializer.endDocument(); - } - private void writeInputGestureToXml(TypedXmlSerializer serializer, InputGestureData inputGestureData) throws IOException { serializer.startTag(null, TAG_INPUT_GESTURE); diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index d2486fe8bd66..87f693cc7291 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -16,6 +16,7 @@ package com.android.server.input; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -32,7 +33,11 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.InputMethodSubtypeHandle; import com.android.internal.policy.IShortcutService; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; import java.util.List; +import java.util.Map; /** * Input manager local system service interface. @@ -41,6 +46,15 @@ import java.util.List; */ public abstract class InputManagerInternal { + // Backup and restore information for custom input gestures. + public static final int BACKUP_CATEGORY_INPUT_GESTURES = 0; + + // Backup and Restore categories for sending map of data back and forth to backup and restore + // infrastructure. + @IntDef({BACKUP_CATEGORY_INPUT_GESTURES}) + public @interface BackupCategory { + } + /** * Called by the display manager to set information about the displays as needed * by the input system. The input system must copy this information to retain it. @@ -312,4 +326,22 @@ public abstract class InputManagerInternal { * @return true if setting power wakeup was successful. */ public abstract boolean setKernelWakeEnabled(int deviceId, boolean enabled); + + /** + * Retrieves the input gestures backup payload data. + * + * @param userId the user ID of the backup data. + * @return byte array of UTF-8 encoded backup data. + */ + public abstract Map<Integer, byte[]> getBackupPayload(int userId) throws IOException; + + /** + * Applies the given UTF-8 encoded byte array payload to the given user's input data + * on a best effort basis. + * + * @param payload UTF-8 encoded map of byte arrays of restored data + * @param userId the user ID for which to apply the payload data + */ + public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId) + throws XmlPullParserException, IOException; } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 2ad5a1538da9..4a5f4a19893a 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -24,6 +24,7 @@ import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT; import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static com.android.hardware.input.Flags.enableCustomizableInputGestures; import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.keyEventActivityDetection; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; @@ -153,6 +154,8 @@ import com.android.server.wm.WindowManagerInternal; import libcore.io.IoUtils; +import org.xmlpull.v1.XmlPullParserException; + import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; @@ -3805,6 +3808,26 @@ public class InputManagerService extends IInputManager.Stub public boolean setKernelWakeEnabled(int deviceId, boolean enabled) { return mNative.setKernelWakeEnabled(deviceId, enabled); } + + @Override + public Map<Integer, byte[]> getBackupPayload(int userId) throws IOException { + final Map<Integer, byte[]> payload = new HashMap<>(); + if (enableCustomizableInputGestures()) { + payload.put(BACKUP_CATEGORY_INPUT_GESTURES, + mKeyGestureController.getInputGestureBackupPayload(userId)); + } + return payload; + } + + @Override + public void applyBackupPayload(Map<Integer, byte[]> payload, int userId) + throws XmlPullParserException, IOException { + if (enableCustomizableInputGestures() && payload.containsKey( + BACKUP_CATEGORY_INPUT_GESTURES)) { + mKeyGestureController.applyInputGesturesBackupPayload( + payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId); + } + } } @Override diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 41f58ae76a4d..5770a09e3b92 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -69,6 +69,11 @@ import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayDeque; import java.util.HashSet; import java.util.List; @@ -1191,6 +1196,29 @@ final class KeyGestureController { } } + byte[] getInputGestureBackupPayload(int userId) throws IOException { + final List<InputGestureData> inputGestureDataList = + mInputGestureManager.getCustomInputGestures(userId, null); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + synchronized (mInputDataStore) { + mInputDataStore.writeInputGestureXml(byteArrayOutputStream, true, inputGestureDataList); + } + return byteArrayOutputStream.toByteArray(); + } + + void applyInputGesturesBackupPayload(byte[] payload, int userId) + throws XmlPullParserException, IOException { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload); + List<InputGestureData> inputGestureDataList; + synchronized (mInputDataStore) { + inputGestureDataList = mInputDataStore.readInputGesturesXml(byteArrayInputStream, true); + } + for (final InputGestureData inputGestureData : inputGestureDataList) { + mInputGestureManager.addCustomInputGesture(userId, inputGestureData); + } + mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); + } + // A record of a registered key gesture event listener from one process. private class KeyGestureEventListenerRecord implements IBinder.DeathRecipient { public final int mPid; diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java index 12495bb4f2cc..d7d0eb40af70 100644 --- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java +++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java @@ -612,25 +612,23 @@ class GnssNetworkConnectivityHandler { networkRequestBuilder.addCapability(getNetworkCapability(mAGpsType)); networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); - if (com.android.internal.telephony.flags.Flags.satelliteInternet()) { - // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network. - TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); - if (telephonyManager != null) { - ServiceState state = telephonyManager.getServiceState(); - if (state != null && state.isUsingNonTerrestrialNetwork()) { - networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); - try { - networkRequestBuilder.addTransportType(NetworkCapabilities - .TRANSPORT_SATELLITE); - networkRequestBuilder.removeCapability(NetworkCapabilities - .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED); - } catch (IllegalArgumentException ignored) { - // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED - // are not recognized, meaning an old connectivity module runs on new - // android in which case no network with such capabilities will be brought - // up, so it's safe to ignore the exception. - // TODO: Can remove the try-catch in next quarter release. - } + // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network. + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + if (telephonyManager != null) { + ServiceState state = telephonyManager.getServiceState(); + if (state != null && state.isUsingNonTerrestrialNetwork()) { + networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + try { + networkRequestBuilder.addTransportType(NetworkCapabilities + .TRANSPORT_SATELLITE); + networkRequestBuilder.removeCapability(NetworkCapabilities + .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED); + } catch (IllegalArgumentException ignored) { + // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED + // are not recognized, meaning an old connectivity module runs on new + // android in which case no network with such capabilities will be brought + // up, so it's safe to ignore the exception. + // TODO: Can remove the try-catch in next quarter release. } } } diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java index e2889fa9cbf6..18bccd8411d7 100644 --- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java +++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java @@ -91,7 +91,7 @@ public class NotificationChannelExtractor implements NotificationSignalExtractor updateAttributes = true; } if (restrictAudioAttributesAlarm() - && record.getNotification().category != CATEGORY_ALARM + && !CATEGORY_ALARM.equals(record.getNotification().category) && attributes.getUsage() == AudioAttributes.USAGE_ALARM) { updateAttributes = true; } diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java index dc1f93664f79..f060e4d11e82 100644 --- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java +++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java @@ -17,8 +17,8 @@ package com.android.server.security; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_BOOT_STATE; -import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS; +import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_PATCH_LEVEL_DIFF; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNKNOWN; @@ -47,12 +47,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.security.AttestationVerificationManagerService.DumpLogger; -import org.json.JSONObject; - import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.cert.CertPath; import java.security.cert.CertPathValidator; @@ -60,7 +56,6 @@ import java.security.cert.CertPathValidatorException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; -import java.security.cert.PKIXCertPathChecker; import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; @@ -69,7 +64,6 @@ import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -126,6 +120,7 @@ class AttestationVerificationPeerDeviceVerifier { private final LocalDate mTestLocalPatchDate; private final CertificateFactory mCertificateFactory; private final CertPathValidator mCertPathValidator; + private final CertificateRevocationStatusManager mCertificateRevocationStatusManager; private final DumpLogger mDumpLogger; AttestationVerificationPeerDeviceVerifier(@NonNull Context context, @@ -135,6 +130,7 @@ class AttestationVerificationPeerDeviceVerifier { mCertificateFactory = CertificateFactory.getInstance("X.509"); mCertPathValidator = CertPathValidator.getInstance("PKIX"); mTrustAnchors = getTrustAnchors(); + mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext); mRevocationEnabled = true; mTestSystemDate = null; mTestLocalPatchDate = null; @@ -150,6 +146,7 @@ class AttestationVerificationPeerDeviceVerifier { mCertificateFactory = CertificateFactory.getInstance("X.509"); mCertPathValidator = CertPathValidator.getInstance("PKIX"); mTrustAnchors = trustAnchors; + mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext); mRevocationEnabled = revocationEnabled; mTestSystemDate = systemDate; mTestLocalPatchDate = localPatchDate; @@ -300,15 +297,14 @@ class AttestationVerificationPeerDeviceVerifier { CertPath certificatePath = mCertificateFactory.generateCertPath(certificates); PKIXParameters validationParams = new PKIXParameters(mTrustAnchors); + // Do not use built-in revocation status checker. + validationParams.setRevocationEnabled(false); + mCertPathValidator.validate(certificatePath, validationParams); if (mRevocationEnabled) { // Checks Revocation Status List based on // https://developer.android.com/training/articles/security-key-attestation#certificate_status - PKIXCertPathChecker checker = new AndroidRevocationStatusListChecker(); - validationParams.addCertPathChecker(checker); + mCertificateRevocationStatusManager.checkRevocationStatus(certificates); } - // Do not use built-in revocation status checker. - validationParams.setRevocationEnabled(false); - mCertPathValidator.validate(certificatePath, validationParams); } private Set<TrustAnchor> getTrustAnchors() throws CertPathValidatorException { @@ -574,96 +570,6 @@ class AttestationVerificationPeerDeviceVerifier { <= maxPatchLevelDiffMonths; } - /** - * Checks certificate revocation status. - * - * Queries status list from android.googleapis.com/attestation/status and checks for - * the existence of certificate's serial number. If serial number exists in map, then fail. - */ - private final class AndroidRevocationStatusListChecker extends PKIXCertPathChecker { - private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries"; - private static final String STATUS_PROPERTY_KEY = "status"; - private static final String REASON_PROPERTY_KEY = "reason"; - private String mStatusUrl; - private JSONObject mJsonStatusMap; - - @Override - public void init(boolean forward) throws CertPathValidatorException { - mStatusUrl = getRevocationListUrl(); - if (mStatusUrl == null || mStatusUrl.isEmpty()) { - throw new CertPathValidatorException( - "R.string.vendor_required_attestation_revocation_list_url is empty."); - } - // TODO(b/221067843): Update to only pull status map on non critical path and if - // out of date (24hrs). - mJsonStatusMap = getStatusMap(mStatusUrl); - } - - @Override - public boolean isForwardCheckingSupported() { - return false; - } - - @Override - public Set<String> getSupportedExtensions() { - return null; - } - - @Override - public void check(Certificate cert, Collection<String> unresolvedCritExts) - throws CertPathValidatorException { - X509Certificate x509Certificate = (X509Certificate) cert; - // The json key is the certificate's serial number converted to lowercase hex. - String serialNumber = x509Certificate.getSerialNumber().toString(16); - - if (serialNumber == null) { - throw new CertPathValidatorException("Certificate serial number can not be null."); - } - - if (mJsonStatusMap.has(serialNumber)) { - JSONObject revocationStatus; - String status; - String reason; - try { - revocationStatus = mJsonStatusMap.getJSONObject(serialNumber); - status = revocationStatus.getString(STATUS_PROPERTY_KEY); - reason = revocationStatus.getString(REASON_PROPERTY_KEY); - } catch (Throwable t) { - throw new CertPathValidatorException("Unable get properties for certificate " - + "with serial number " + serialNumber); - } - throw new CertPathValidatorException( - "Invalid certificate with serial number " + serialNumber - + " has status " + status - + " because reason " + reason); - } - } - - private JSONObject getStatusMap(String stringUrl) throws CertPathValidatorException { - URL url; - try { - url = new URL(stringUrl); - } catch (Throwable t) { - throw new CertPathValidatorException( - "Unable to get revocation status from " + mStatusUrl, t); - } - - try (InputStream inputStream = url.openStream()) { - JSONObject statusListJson = new JSONObject( - new String(inputStream.readAllBytes(), UTF_8)); - return statusListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY); - } catch (Throwable t) { - throw new CertPathValidatorException( - "Unable to parse revocation status from " + mStatusUrl, t); - } - } - - private String getRevocationListUrl() { - return mContext.getResources().getString( - R.string.vendor_required_attestation_revocation_list_url); - } - } - /* Mutable data class for tracking dump data from verifications. */ private static class MyDumpData extends AttestationVerificationManagerService.DumpData { diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java new file mode 100644 index 000000000000..d36d9f5f6636 --- /dev/null +++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2025 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.security; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.Context; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Environment; +import android.os.PersistableBundle; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.cert.CertPathValidatorException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Manages the revocation status of certificates used in remote attestation. */ +class CertificateRevocationStatusManager { + private static final String TAG = "AVF_CRL"; + // Must be unique within system server + private static final int JOB_ID = 1737671340; + private static final String REVOCATION_STATUS_FILE_NAME = "certificate_revocation_status.txt"; + private static final String REVOCATION_STATUS_FILE_FIELD_DELIMITER = ","; + + /** + * The number of days since last update for which a stored revocation status can be accepted. + */ + @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30; + + /** + * The number of days since issue date for an intermediary certificate to be considered fresh + * and not require a revocation list check. + */ + private static final int FRESH_INTERMEDIARY_CERT_DAYS = 70; + + /** + * The expected number of days between a certificate's issue date and notBefore date. Used to + * infer a certificate's issue date from its notBefore date. + */ + private static final int DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES = 2; + + private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries"; + private static final Object sFileLock = new Object(); + + private final Context mContext; + private final String mTestRemoteRevocationListUrl; + private final File mTestRevocationStatusFile; + private final boolean mShouldScheduleJob; + + CertificateRevocationStatusManager(Context context) { + this(context, null, null, true); + } + + @VisibleForTesting + CertificateRevocationStatusManager( + Context context, + String testRemoteRevocationListUrl, + File testRevocationStatusFile, + boolean shouldScheduleJob) { + mContext = context; + mTestRemoteRevocationListUrl = testRemoteRevocationListUrl; + mTestRevocationStatusFile = testRevocationStatusFile; + mShouldScheduleJob = shouldScheduleJob; + } + + /** + * Check the revocation status of the provided {@link X509Certificate}s. + * + * <p>The provided certificates should have been validated and ordered from leaf to a + * certificate issued by the trust anchor, per the convention specified in the javadoc of {@link + * java.security.cert.CertPath}. + * + * @param certificates List of certificates to be checked + * @throws CertPathValidatorException if the check failed + */ + void checkRevocationStatus(List<X509Certificate> certificates) + throws CertPathValidatorException { + if (!needToCheckRevocationStatus(certificates)) { + return; + } + List<String> serialNumbers = new ArrayList<>(); + for (X509Certificate certificate : certificates) { + String serialNumber = certificate.getSerialNumber().toString(16); + if (serialNumber == null) { + throw new CertPathValidatorException("Certificate serial number cannot be null."); + } + serialNumbers.add(serialNumber); + } + try { + JSONObject revocationList = fetchRemoteRevocationList(); + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (String serialNumber : serialNumbers) { + areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber)); + } + updateLastRevocationCheckData(areCertificatesRevoked); + for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) { + if (entry.getValue()) { + throw new CertPathValidatorException( + "Certificate " + entry.getKey() + " has been revoked."); + } + } + } catch (IOException | JSONException ex) { + Slog.d(TAG, "Fallback to check stored revocation status", ex); + if (ex instanceof IOException && mShouldScheduleJob) { + scheduleJobToUpdateStoredDataWithRemoteRevocationList(serialNumbers); + } + for (X509Certificate certificate : certificates) { + // Assume recently issued certificates are not revoked. + if (isIssuedWithinDays(certificate, MAX_DAYS_SINCE_LAST_CHECK)) { + String serialNumber = certificate.getSerialNumber().toString(16); + serialNumbers.remove(serialNumber); + } + } + Map<String, LocalDateTime> lastRevocationCheckData; + try { + lastRevocationCheckData = getLastRevocationCheckData(); + } catch (IOException ex2) { + throw new CertPathValidatorException( + "Unable to load stored revocation status", ex2); + } + for (String serialNumber : serialNumbers) { + if (!lastRevocationCheckData.containsKey(serialNumber) + || lastRevocationCheckData + .get(serialNumber) + .isBefore( + LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) { + throw new CertPathValidatorException( + "Unable to verify the revocation status of certificate " + + serialNumber); + } + } + } + } + + private static boolean needToCheckRevocationStatus( + List<X509Certificate> certificatesOrderedLeafFirst) { + if (certificatesOrderedLeafFirst.isEmpty()) { + return false; + } + // A certificate isn't revoked when it is first issued, so we treat it as checked on its + // issue date. + if (!isIssuedWithinDays(certificatesOrderedLeafFirst.get(0), MAX_DAYS_SINCE_LAST_CHECK)) { + return true; + } + for (int i = 1; i < certificatesOrderedLeafFirst.size(); i++) { + if (!isIssuedWithinDays( + certificatesOrderedLeafFirst.get(i), FRESH_INTERMEDIARY_CERT_DAYS)) { + return true; + } + } + return false; + } + + private static boolean isIssuedWithinDays(X509Certificate certificate, int days) { + LocalDate notBeforeDate = + LocalDate.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()); + LocalDate expectedIssueData = + notBeforeDate.plusDays(DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES); + return LocalDate.now().minusDays(days + 1).isBefore(expectedIssueData); + } + + void updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + JSONObject revocationList, Collection<String> otherCertificatesToCheck) { + Set<String> allCertificatesToCheck = new HashSet<>(otherCertificatesToCheck); + try { + allCertificatesToCheck.addAll(getLastRevocationCheckData().keySet()); + } catch (IOException ex) { + Slog.e(TAG, "Unable to update last check date of stored data.", ex); + } + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (String serialNumber : allCertificatesToCheck) { + areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber)); + } + updateLastRevocationCheckData(areCertificatesRevoked); + } + + /** + * Update the last revocation check data stored on this device. + * + * @param areCertificatesRevoked A Map whose keys are certificate serial numbers and values are + * whether that certificate has been revoked + */ + void updateLastRevocationCheckData(Map<String, Boolean> areCertificatesRevoked) { + LocalDateTime now = LocalDateTime.now(); + synchronized (sFileLock) { + Map<String, LocalDateTime> lastRevocationCheckData; + try { + lastRevocationCheckData = getLastRevocationCheckData(); + } catch (IOException ex) { + Slog.e(TAG, "Unable to updateLastRevocationCheckData", ex); + return; + } + for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) { + if (entry.getValue()) { + lastRevocationCheckData.remove(entry.getKey()); + } else { + lastRevocationCheckData.put(entry.getKey(), now); + } + } + storeLastRevocationCheckData(lastRevocationCheckData); + } + } + + Map<String, LocalDateTime> getLastRevocationCheckData() throws IOException { + Map<String, LocalDateTime> data = new HashMap<>(); + File dataFile = getLastRevocationCheckDataFile(); + synchronized (sFileLock) { + if (!dataFile.exists()) { + return data; + } + String dataString; + try (FileInputStream in = new FileInputStream(dataFile)) { + dataString = new String(in.readAllBytes(), UTF_8); + } + for (String line : dataString.split(System.lineSeparator())) { + String[] elements = line.split(REVOCATION_STATUS_FILE_FIELD_DELIMITER); + if (elements.length != 2) { + continue; + } + try { + data.put(elements[0], LocalDateTime.parse(elements[1])); + } catch (DateTimeParseException ex) { + Slog.e( + TAG, + "Unable to parse last checked LocalDateTime from file. Deleting the" + + " potentially corrupted file.", + ex); + dataFile.delete(); + return data; + } + } + } + return data; + } + + @VisibleForTesting + void storeLastRevocationCheckData(Map<String, LocalDateTime> lastRevocationCheckData) { + StringBuilder dataStringBuilder = new StringBuilder(); + for (Map.Entry<String, LocalDateTime> entry : lastRevocationCheckData.entrySet()) { + dataStringBuilder + .append(entry.getKey()) + .append(REVOCATION_STATUS_FILE_FIELD_DELIMITER) + .append(entry.getValue()) + .append(System.lineSeparator()); + } + synchronized (sFileLock) { + try (FileOutputStream fileOutputStream = + new FileOutputStream(getLastRevocationCheckDataFile())) { + fileOutputStream.write(dataStringBuilder.toString().getBytes(UTF_8)); + Slog.d(TAG, "Successfully stored revocation status data."); + } catch (IOException ex) { + Slog.e(TAG, "Failed to store revocation status data.", ex); + } + } + } + + private File getLastRevocationCheckDataFile() { + if (mTestRevocationStatusFile != null) { + return mTestRevocationStatusFile; + } + return new File(Environment.getDataSystemDirectory(), REVOCATION_STATUS_FILE_NAME); + } + + private void scheduleJobToUpdateStoredDataWithRemoteRevocationList(List<String> serialNumbers) { + JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Slog.e(TAG, "Unable to get job scheduler."); + return; + } + Slog.d(TAG, "Scheduling job to fetch remote CRL."); + PersistableBundle extras = new PersistableBundle(); + extras.putStringArray( + UpdateCertificateRevocationStatusJobService.EXTRA_KEY_CERTIFICATES_TO_CHECK, + serialNumbers.toArray(new String[0])); + jobScheduler.schedule( + new JobInfo.Builder( + JOB_ID, + new ComponentName( + mContext, + UpdateCertificateRevocationStatusJobService.class)) + .setExtras(extras) + .setRequiredNetwork( + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build()) + .build()); + } + + /** + * Fetches the revocation list from the URL specified in + * R.string.vendor_required_attestation_revocation_list_url + * + * @return The remote revocation list entries in a JSONObject + * @throws CertPathValidatorException if the URL is not defined or is malformed. + * @throws IOException if the URL is valid but the fetch failed. + * @throws JSONException if the revocation list content cannot be parsed + */ + JSONObject fetchRemoteRevocationList() + throws CertPathValidatorException, IOException, JSONException { + String urlString = getRemoteRevocationListUrl(); + if (urlString == null || urlString.isEmpty()) { + throw new CertPathValidatorException( + "R.string.vendor_required_attestation_revocation_list_url is empty."); + } + URL url; + try { + url = new URL(urlString); + } catch (MalformedURLException ex) { + throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex); + } + byte[] revocationListBytes; + try (InputStream inputStream = url.openStream()) { + revocationListBytes = inputStream.readAllBytes(); + } + JSONObject revocationListJson = new JSONObject(new String(revocationListBytes, UTF_8)); + return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY); + } + + private String getRemoteRevocationListUrl() { + if (mTestRemoteRevocationListUrl != null) { + return mTestRemoteRevocationListUrl; + } + return mContext.getResources() + .getString(R.string.vendor_required_attestation_revocation_list_url); + } +} diff --git a/services/core/java/com/android/server/security/OWNERS b/services/core/java/com/android/server/security/OWNERS index fa4bf228c683..7a31a0006bb9 100644 --- a/services/core/java/com/android/server/security/OWNERS +++ b/services/core/java/com/android/server/security/OWNERS @@ -3,5 +3,6 @@ include /core/java/android/security/OWNERS per-file *AttestationVerification* = file:/core/java/android/security/attestationverification/OWNERS +per-file *CertificateRevocationStatus* = file:/core/java/android/security/attestationverification/OWNERS per-file FileIntegrity*.java = victorhsieh@google.com per-file KeyChainSystemService.java = file:platform/packages/apps/KeyChain:/OWNERS diff --git a/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java new file mode 100644 index 000000000000..768c812f47a3 --- /dev/null +++ b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2025 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.security; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.util.Slog; + +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** A {@link JobService} that fetches the certificate revocation list from a remote location. */ +public class UpdateCertificateRevocationStatusJobService extends JobService { + + static final String EXTRA_KEY_CERTIFICATES_TO_CHECK = + "com.android.server.security.extra.CERTIFICATES_TO_CHECK"; + private static final String TAG = "AVF_CRL"; + private ExecutorService mExecutorService; + + @Override + public void onCreate() { + super.onCreate(); + mExecutorService = Executors.newSingleThreadExecutor(); + } + + @Override + public boolean onStartJob(JobParameters params) { + mExecutorService.execute( + () -> { + try { + CertificateRevocationStatusManager certificateRevocationStatusManager = + new CertificateRevocationStatusManager(this); + Slog.d(TAG, "Starting to fetch remote CRL from job service."); + JSONObject revocationList = + certificateRevocationStatusManager.fetchRemoteRevocationList(); + String[] certificatesToCheckFromJobParams = + params.getExtras().getStringArray(EXTRA_KEY_CERTIFICATES_TO_CHECK); + if (certificatesToCheckFromJobParams == null) { + Slog.e(TAG, "Extras not found: " + EXTRA_KEY_CERTIFICATES_TO_CHECK); + return; + } + certificateRevocationStatusManager + .updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + revocationList, + Arrays.asList(certificatesToCheckFromJobParams)); + } catch (Throwable t) { + Slog.e(TAG, "Unable to update the stored revocation status.", t); + } + jobFinished(params, false); + }); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mExecutorService.shutdown(); + } +} diff --git a/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java new file mode 100644 index 000000000000..9d60a576d9bc --- /dev/null +++ b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 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.storage; + +import android.content.Context; +import android.os.storage.DiskInfo; +import android.os.storage.StorageVolume; +import android.os.storage.VolumeInfo; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.File; + +/** + * An immutable version of {@link VolumeInfo} with only getters. + * + * @hide + */ +public final class ImmutableVolumeInfo { + private final VolumeInfo mVolumeInfo; + + private ImmutableVolumeInfo(VolumeInfo volumeInfo) { + mVolumeInfo = new VolumeInfo(volumeInfo); + } + + public static ImmutableVolumeInfo fromVolumeInfo(VolumeInfo info) { + return new ImmutableVolumeInfo(info); + } + + public ImmutableVolumeInfo clone() { + return fromVolumeInfo(mVolumeInfo.clone()); + } + + public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { + return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted); + } + + public void dump(IndentingPrintWriter pw) { + mVolumeInfo.dump(pw); + } + + public DiskInfo getDisk() { + return mVolumeInfo.getDisk(); + } + + public String getDiskId() { + return mVolumeInfo.getDiskId(); + } + + public String getFsLabel() { + return mVolumeInfo.fsLabel; + } + + public String getFsPath() { + return mVolumeInfo.path; + } + + public String getFsType() { + return mVolumeInfo.fsType; + } + + public String getFsUuid() { + return mVolumeInfo.fsUuid; + } + + public String getId() { + return mVolumeInfo.id; + } + + public File getInternalPath() { + return mVolumeInfo.getInternalPath(); + } + + public int getMountFlags() { + return mVolumeInfo.mountFlags; + } + + public int getMountUserId() { + return mVolumeInfo.mountUserId; + } + + public String getPartGuid() { + return mVolumeInfo.partGuid; + } + + public File getPath() { + return mVolumeInfo.getPath(); + } + + public int getState() { + return mVolumeInfo.state; + } + + public int getType() { + return mVolumeInfo.type; + } + + public VolumeInfo getVolumeInfo() { + return new VolumeInfo(mVolumeInfo); // Return a copy, not the original + } + + public boolean isMountedReadable() { + return mVolumeInfo.isMountedReadable(); + } + + public boolean isMountedWritable() { + return mVolumeInfo.isMountedWritable(); + } + + public boolean isPrimary() { + return mVolumeInfo.isPrimary(); + } + + public boolean isVisible() { + return mVolumeInfo.isVisible(); + } + + public boolean isVisibleForUser(int userId) { + return mVolumeInfo.isVisibleForUser(userId); + } + + public boolean isVisibleForWrite(int userId) { + return mVolumeInfo.isVisibleForWrite(userId); + } +} diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java index b9c9b64cd2c6..342b864c6473 100644 --- a/services/core/java/com/android/server/storage/StorageSessionController.java +++ b/services/core/java/com/android/server/storage/StorageSessionController.java @@ -45,6 +45,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.server.storage.ImmutableVolumeInfo; import java.util.Objects; @@ -79,18 +80,18 @@ public final class StorageSessionController { * @param vol for which the storage session has to be started * @return userId for connection for this volume */ - public int getConnectionUserIdForVolume(VolumeInfo vol) { + public int getConnectionUserIdForVolume(ImmutableVolumeInfo vol) { final Context volumeUserContext = mContext.createContextAsUser( - UserHandle.of(vol.mountUserId), 0); + UserHandle.of(vol.getMountUserId()), 0); boolean isMediaSharedWithParent = volumeUserContext.getSystemService( UserManager.class).isMediaSharedWithParent(); - UserInfo userInfo = mUserManager.getUserInfo(vol.mountUserId); + UserInfo userInfo = mUserManager.getUserInfo(vol.getMountUserId()); if (userInfo != null && isMediaSharedWithParent) { // Clones use the same connection as their parent return userInfo.profileGroupId; } else { - return vol.mountUserId; + return vol.getMountUserId(); } } @@ -108,7 +109,7 @@ public final class StorageSessionController { * @throws ExternalStorageServiceException if the session fails to start * @throws IllegalStateException if a session has already been created for {@code vol} */ - public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol) + public void onVolumeMount(ParcelFileDescriptor deviceFd, ImmutableVolumeInfo vol) throws ExternalStorageServiceException { if (!shouldHandle(vol)) { return; @@ -144,7 +145,8 @@ public final class StorageSessionController { * * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService */ - public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException { + public void notifyVolumeStateChanged(ImmutableVolumeInfo vol) + throws ExternalStorageServiceException { if (!shouldHandle(vol)) { return; } @@ -214,7 +216,7 @@ public final class StorageSessionController { * @return the connection that was removed or {@code null} if nothing was removed */ @Nullable - public StorageUserConnection onVolumeRemove(VolumeInfo vol) { + public StorageUserConnection onVolumeRemove(ImmutableVolumeInfo vol) { if (!shouldHandle(vol)) { return null; } @@ -246,7 +248,7 @@ public final class StorageSessionController { * * Call {@link #onVolumeRemove} to remove the connection without waiting for exit */ - public void onVolumeUnmount(VolumeInfo vol) { + public void onVolumeUnmount(ImmutableVolumeInfo vol) { String sessionId = vol.getId(); final long token = Binder.clearCallingIdentity(); try { @@ -457,9 +459,9 @@ public final class StorageSessionController { * Returns {@code true} if {@code vol} is an emulated or visible public volume, * {@code false} otherwise **/ - public static boolean isEmulatedOrPublic(VolumeInfo vol) { - return vol.type == VolumeInfo.TYPE_EMULATED - || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); + public static boolean isEmulatedOrPublic(ImmutableVolumeInfo vol) { + return vol.getType() == VolumeInfo.TYPE_EMULATED + || (vol.getType() == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); } /** Exception thrown when communication with the {@link ExternalStorageService} fails. */ @@ -477,11 +479,11 @@ public final class StorageSessionController { } } - private static boolean isSupportedVolume(VolumeInfo vol) { - return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB; + private static boolean isSupportedVolume(ImmutableVolumeInfo vol) { + return isEmulatedOrPublic(vol) || vol.getType() == VolumeInfo.TYPE_STUB; } - private boolean shouldHandle(@Nullable VolumeInfo vol) { + private boolean shouldHandle(@Nullable ImmutableVolumeInfo vol) { return !mIsResetting && (vol == null || isSupportedVolume(vol)); } diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java new file mode 100644 index 000000000000..4124cfb4f092 --- /dev/null +++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java @@ -0,0 +1,206 @@ +/* + * 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.storage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.storage.DiskInfo; +import android.os.storage.StorageVolume; +import android.os.storage.VolumeInfo; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.utils.Watchable; +import com.android.server.utils.WatchableImpl; + +import java.io.File; + +/** + * A wrapper for {@link VolumeInfo} implementing the {@link Watchable} interface. + * + * The {@link VolumeInfo} class itself cannot safely implement Watchable, because it has several + * UnsupportedAppUsage annotations and public fields, which allow it to be modified without + * notifying watchers. + * + * @hide + */ +public class WatchedVolumeInfo extends WatchableImpl { + private final VolumeInfo mVolumeInfo; + + private WatchedVolumeInfo(VolumeInfo volumeInfo) { + mVolumeInfo = volumeInfo; + } + + public WatchedVolumeInfo(WatchedVolumeInfo watchedVolumeInfo) { + mVolumeInfo = new VolumeInfo(watchedVolumeInfo.mVolumeInfo); + } + + public static WatchedVolumeInfo fromVolumeInfo(VolumeInfo info) { + return new WatchedVolumeInfo(info); + } + + /** + * Returns a copy of the embedded VolumeInfo object, to be used by components + * that just need it for retrieving some state from it. + * + * @return A copy of the embedded VolumeInfo object + */ + + public WatchedVolumeInfo clone() { + return fromVolumeInfo(mVolumeInfo.clone()); + } + + public ImmutableVolumeInfo getImmutableVolumeInfo() { + return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo); + } + + public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { + return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted); + } + + public void dump(IndentingPrintWriter pw) { + mVolumeInfo.dump(pw); + } + + public DiskInfo getDisk() { + return mVolumeInfo.getDisk(); + } + + public String getDiskId() { + return mVolumeInfo.getDiskId(); + } + + public String getFsLabel() { + return mVolumeInfo.fsLabel; + } + + public void setFsLabel(String fsLabel) { + mVolumeInfo.fsLabel = fsLabel; + dispatchChange(this); + } + + public String getFsPath() { + return mVolumeInfo.path; + } + + public void setFsPath(String path) { + mVolumeInfo.path = path; + dispatchChange(this); + } + + public String getFsType() { + return mVolumeInfo.fsType; + } + + public void setFsType(String fsType) { + mVolumeInfo.fsType = fsType; + dispatchChange(this); + } + + public @Nullable String getFsUuid() { + return mVolumeInfo.fsUuid; + } + + public void setFsUuid(String fsUuid) { + mVolumeInfo.fsUuid = fsUuid; + dispatchChange(this); + } + + public @NonNull String getId() { + return mVolumeInfo.id; + } + + public File getInternalPath() { + return mVolumeInfo.getInternalPath(); + } + + public void setInternalPath(String internalPath) { + mVolumeInfo.internalPath = internalPath; + dispatchChange(this); + } + + public int getMountFlags() { + return mVolumeInfo.mountFlags; + } + + public void setMountFlags(int mountFlags) { + mVolumeInfo.mountFlags = mountFlags; + dispatchChange(this); + } + + public int getMountUserId() { + return mVolumeInfo.mountUserId; + } + + public void setMountUserId(int mountUserId) { + mVolumeInfo.mountUserId = mountUserId; + dispatchChange(this); + } + + public String getPartGuid() { + return mVolumeInfo.partGuid; + } + + public File getPath() { + return mVolumeInfo.getPath(); + } + + public int getState() { + return mVolumeInfo.state; + } + + public int getState(int state) { + return mVolumeInfo.state; + } + + public void setState(int state) { + mVolumeInfo.state = state; + dispatchChange(this); + } + + public int getType() { + return mVolumeInfo.type; + } + + public VolumeInfo getVolumeInfo() { + return new VolumeInfo(mVolumeInfo); + } + + public boolean isMountedReadable() { + return mVolumeInfo.isMountedReadable(); + } + + public boolean isMountedWritable() { + return mVolumeInfo.isMountedWritable(); + } + + public boolean isPrimary() { + return mVolumeInfo.isPrimary(); + } + + public boolean isVisible() { + return mVolumeInfo.isVisible(); + } + + public boolean isVisibleForUser(int userId) { + return mVolumeInfo.isVisibleForUser(userId); + } + + public boolean isVisibleForWrite(int userId) { + return mVolumeInfo.isVisibleForWrite(userId); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b17eef85f93d..d84016b3816e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -246,9 +246,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WIND import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES; -import static com.android.server.wm.WindowManagerService.sEnableShellTransitions; import static com.android.server.wm.WindowState.LEGACY_POLICY_VISIBILITY; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; @@ -764,13 +762,6 @@ final class ActivityRecord extends WindowToken { boolean mLastImeShown; /** - * When set to true, the IME insets will be frozen until the next app becomes IME input target. - * @see InsetsPolicy#adjustVisibilityForIme - * @see ImeInsetsSourceProvider#updateClientVisibility - */ - boolean mImeInsetsFrozenUntilStartInput; - - /** * A flag to determine if this AR is in the process of closing or entering PIP. This is needed * to help AR know that the app is in the process of closing but hasn't yet started closing on * the WM side. @@ -1175,8 +1166,6 @@ final class ActivityRecord extends WindowToken { pw.print(" launchMode="); pw.println(launchMode); pw.print(prefix); pw.print("mActivityType="); pw.println(activityTypeToString(getActivityType())); - pw.print(prefix); pw.print("mImeInsetsFrozenUntilStartInput="); - pw.println(mImeInsetsFrozenUntilStartInput); if (requestedVrComponent != null) { pw.print(prefix); pw.print("requestedVrComponent="); @@ -5239,7 +5228,8 @@ final class ActivityRecord extends WindowToken { pendingOptions.getWidth(), pendingOptions.getHeight()); options = AnimationOptions.makeScaleUpAnimOptions( pendingOptions.getStartX(), pendingOptions.getStartY(), - pendingOptions.getWidth(), pendingOptions.getHeight()); + pendingOptions.getWidth(), pendingOptions.getHeight(), + pendingOptions.getOverrideTaskTransition()); if (intent.getSourceBounds() == null) { intent.setSourceBounds(new Rect(pendingOptions.getStartX(), pendingOptions.getStartY(), @@ -5771,19 +5761,16 @@ final class ActivityRecord extends WindowToken { return; } - final int windowsCount = mChildren.size(); - // With Shell-Transition, the activity will running a transition when it is visible. - // It won't be included when fromTransition is true means the call from finishTransition. - final boolean runningAnimation = sEnableShellTransitions ? visible - : isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION); - for (int i = 0; i < windowsCount; i++) { - mChildren.get(i).onAppVisibilityChanged(visible, runningAnimation); + if (!visible) { + for (int i = mChildren.size() - 1; i >= 0; --i) { + mChildren.get(i).onAppCommitInvisible(); + } } setVisible(visible); setVisibleRequested(visible); ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "commitVisibility: %s: visible=%b" - + " visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s", - this, isVisible(), mVisibleRequested, isInTransition(), runningAnimation, + + " visibleRequested=%b, inTransition=%b, caller=%s", + this, visible, mVisibleRequested, inTransition(), Debug.getCallers(5)); if (visible) { // If we are being set visible, and the starting window is not yet displayed, @@ -5873,10 +5860,6 @@ final class ActivityRecord extends WindowToken { } final DisplayContent displayContent = getDisplayContent(); - if (!visible) { - mImeInsetsFrozenUntilStartInput = true; - } - if (!displayContent.mClosingApps.contains(this) && !displayContent.mOpeningApps.contains(this) && !fromTransition) { @@ -6224,13 +6207,8 @@ final class ActivityRecord extends WindowToken { return false; } - // Hide all activities on the presenting display so that malicious apps can't do tap - // jacking (b/391466268). - // For now, this should only be applied to external displays because presentations can only - // be shown on them. - // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that - // the presentation won't stop its controlling activity. - if (enablePresentationForConnectedDisplays() && mDisplayContent.mIsPresenting) { + // A presentation stopps all activities behind on the same display. + if (mWmService.mPresentationController.shouldOccludeActivities(getDisplayId())) { return false; } @@ -6952,14 +6930,6 @@ final class ActivityRecord extends WindowToken { // closing activity having to wait until idle timeout to be stopped or destroyed if the // next activity won't report idle (e.g. repeated view animation). mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded(); - - // If the activity is visible, but no windows are eligible to start input, unfreeze - // to avoid permanently frozen IME insets. - if (mImeInsetsFrozenUntilStartInput && getWindow( - win -> WindowManager.LayoutParams.mayUseInputMethod(win.mAttrs.flags)) - == null) { - mImeInsetsFrozenUntilStartInput = false; - } } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index ddb9f178cb8b..254127dee7a8 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -4324,10 +4324,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { task = mRootWindowContainer.getDefaultTaskDisplayArea().getRootTask( t -> t.isActivityTypeStandard()); } - if (task != null && task.getTopMostActivity() != null - && !task.getTopMostActivity().isState(FINISHING, DESTROYING, DESTROYED)) { + final ActivityRecord topActivity = task != null + ? task.getTopMostActivity() + : null; + if (topActivity != null && !topActivity.isState(FINISHING, DESTROYING, DESTROYED)) { mWindowManager.mAtmService.mActivityClientController - .onPictureInPictureUiStateChanged(task.getTopMostActivity(), pipState); + .onPictureInPictureUiStateChanged(topActivity, pipState); } } } diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java index 576e5d5d0cd2..439b503c0c57 100644 --- a/services/core/java/com/android/server/wm/AppWarnings.java +++ b/services/core/java/com/android/server/wm/AppWarnings.java @@ -506,6 +506,10 @@ class AppWarnings { context = new ContextThemeWrapper(context, context.getThemeResId()) { @Override public void startActivity(Intent intent) { + // PageSizeMismatch dialog stays on top of the browser even after opening link + // set broadcast to close the dialog when link has been clicked. + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); super.startActivity(intent); } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 79bed3d8453d..e76a83453a9d 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -388,8 +388,7 @@ class BackNavigationController { removedWindowContainer); mBackAnimationInProgress = builder != null; if (mBackAnimationInProgress) { - if (removedWindowContainer.mTransitionController.inTransition() - || mWindowManagerService.mSyncEngine.hasPendingSyncSets()) { + if (removedWindowContainer.mTransitionController.inTransition()) { ProtoLog.w(WM_DEBUG_BACK_PREVIEW, "Pending back animation due to another animation is running"); mPendingAnimationBuilder = builder; @@ -817,6 +816,8 @@ class BackNavigationController { if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */) && ar.mTransitionController.isCollecting(ar)) { final TransitionController controller = ar.mTransitionController; + final Transition transition = controller.getCollectingTransition(); + final int switchType = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType; boolean collectTask = false; ActivityRecord changedActivity = null; for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { @@ -829,8 +830,16 @@ class BackNavigationController { changedActivity = next; } } - if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType - == AnimationHandler.TASK_SWITCH) { + if (Flags.unifyBackNavigationTransition()) { + for (int i = mAnimationHandler.mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) { + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mOpenAnimAdaptor.mAdaptors[i].mTarget, + false /* isTop */); + } + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mCloseAdaptor.mTarget, true /* isTop */); + } + if (collectTask && switchType == AnimationHandler.TASK_SWITCH) { final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask(); if (topTask != null) { WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent(); @@ -848,6 +857,18 @@ class BackNavigationController { } } + private static void collectAnimatableTarget(Transition transition, int switchType, + WindowContainer animatingTarget, boolean isTop) { + if ((switchType == AnimationHandler.ACTIVITY_SWITCH + && (animatingTarget.asActivityRecord() != null + || animatingTarget.asTaskFragment() != null)) + || (switchType == AnimationHandler.TASK_SWITCH + && animatingTarget.asTask() != null)) { + transition.collect(animatingTarget); + transition.setBackGestureAnimation(animatingTarget, isTop); + } + } + // For shell transition /** * Check whether the transition targets was animated by back gesture animation. @@ -992,8 +1013,8 @@ class BackNavigationController { return; } - if (mWindowManagerService.mRoot.mTransitionController.isCollecting()) { - Slog.v(TAG, "Skip predictive back transition, another transition is collecting"); + if (mWindowManagerService.mRoot.mTransitionController.inTransition()) { + Slog.v(TAG, "Skip predictive back transition, another transition is playing"); cancelPendingAnimation(); return; } @@ -1098,7 +1119,7 @@ class BackNavigationController { } final Transition prepareTransition = builder.prepareTransitionIfNeeded( - openingActivities); + openingActivities, close, open); final SurfaceControl.Transaction st = openingActivities[0].getSyncTransaction(); final SurfaceControl.Transaction ct = prepareTransition != null ? st : close.getPendingTransaction(); @@ -1790,7 +1811,8 @@ class BackNavigationController { return wc == mCloseTarget || mCloseTarget.hasChild(wc) || wc.hasChild(mCloseTarget); } - private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities) { + private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities, + WindowContainer promoteToClose, WindowContainer[] promoteToOpen) { if (Flags.unifyBackNavigationTransition()) { if (mCloseTarget.asWindowState() != null) { return null; @@ -1806,11 +1828,11 @@ class BackNavigationController { final TransitionController tc = visibleOpenActivities[0].mTransitionController; final Transition prepareOpen = tc.createTransition( TRANSIT_PREPARE_BACK_NAVIGATION); - tc.collect(mCloseTarget); - prepareOpen.setBackGestureAnimation(mCloseTarget, true /* isTop */); - for (int i = mOpenTargets.length - 1; i >= 0; --i) { - tc.collect(mOpenTargets[i]); - prepareOpen.setBackGestureAnimation(mOpenTargets[i], false /* isTop */); + tc.collect(promoteToClose); + prepareOpen.setBackGestureAnimation(promoteToClose, true /* isTop */); + for (int i = promoteToOpen.length - 1; i >= 0; --i) { + tc.collect(promoteToOpen[i]); + prepareOpen.setBackGestureAnimation(promoteToOpen[i], false /* isTop */); } if (!makeVisibles.isEmpty()) { setLaunchBehind(visibleOpenActivities); diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index a1faa7573a0c..f35930700653 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -51,8 +51,13 @@ public final class DesktopModeHelper { } /** - * Return {@code true} if the current device supports desktop mode. + * Return {@code true} if the current device can hosts desktop sessions on its internal display. */ + @VisibleForTesting + static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + // TODO(b/337819319): use a companion object instead. private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); @@ -67,12 +72,12 @@ public final class DesktopModeHelper { */ private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported( - context) || isDeviceEligibleForDesktopMode(context)); + context) || isInternalDisplayEligibleToHostDesktops(context)); } @VisibleForTesting - static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { + return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported( context)); } @@ -81,12 +86,14 @@ public final class DesktopModeHelper { * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return (isDesktopModeEnabled() && isDeviceEligibleForDesktopMode(context)) + return (isInternalDisplayEligibleToHostDesktops(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() + && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions())) || isDesktopModeEnabledByDevOption(context); } /** Returns {@code true} if desktop experience wallpaper is supported on this device. */ public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) { - return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context); + return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context); } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 1dd7c4d4adbd..c87087f84399 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -547,9 +547,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // TODO(multi-display): remove some of the usages. boolean isDefaultDisplay; - /** Indicates whether any presentation is shown on this display. */ - boolean mIsPresenting; - /** Save allocating when calculating rects */ private final Rect mTmpRect = new Rect(); private final Region mTmpRegion = new Region(); @@ -4661,35 +4658,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } - /** - * Callback from {@link ImeInsetsSourceProvider#updateClientVisibility} for the system to - * judge whether or not to notify the IME insets provider to dispatch this reported IME client - * visibility state to the app clients when needed. - */ - boolean onImeInsetsClientVisibilityUpdate() { - boolean[] changed = new boolean[1]; - - // Unlike the IME layering target or the control target can be updated during the layout - // change, the IME input target requires to be changed after gaining the input focus. - // In case unfreezing IME insets state may too early during IME focus switching, we unfreeze - // when activities going to be visible until the input target changed, or the - // activity was the current input target that has to unfreeze after updating the IME - // client visibility. - final ActivityRecord inputTargetActivity = - mImeInputTarget != null ? mImeInputTarget.getActivityRecord() : null; - final boolean targetChanged = mImeInputTarget != mLastImeInputTarget; - if (targetChanged || inputTargetActivity != null && inputTargetActivity.isVisibleRequested() - && inputTargetActivity.mImeInsetsFrozenUntilStartInput) { - forAllActivities(r -> { - if (r.mImeInsetsFrozenUntilStartInput && r.isVisibleRequested()) { - r.mImeInsetsFrozenUntilStartInput = false; - changed[0] = true; - } - }); - } - return changed[0]; - } - void updateImeControlTarget() { updateImeControlTarget(false /* forceUpdateImeParent */); } @@ -7141,14 +7109,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void updateRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) { - int newRequestedVisibleTypes = + @InsetsType int updateRequestedVisibleTypes( + @InsetsType int visibleTypes, @InsetsType int mask) { + final int newRequestedVisibleTypes = (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask); if (mRequestedVisibleTypes != newRequestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ newRequestedVisibleTypes; mRequestedVisibleTypes = newRequestedVisibleTypes; + return changedTypes; } + return 0; } } diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 907d0dc2e183..7b6fc9e5694d 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -34,6 +34,7 @@ import android.util.proto.ProtoOutputStream; import android.view.InputApplicationHandle; import android.view.InputChannel; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.window.InputTransferToken; import com.android.internal.protolog.ProtoLog; @@ -260,7 +261,7 @@ class EmbeddedWindowController { // The EmbeddedWindow can only request the IME. All other insets types are requested by // the host window. - private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0; + private @InsetsType int mRequestedVisibleTypes = 0; /** Whether the gesture is transferred to embedded window. */ boolean mGestureToEmbedded = false; @@ -354,24 +355,28 @@ class EmbeddedWindowController { } @Override - public boolean isRequestedVisible(@WindowInsets.Type.InsetsType int types) { + public boolean isRequestedVisible(@InsetsType int types) { return (mRequestedVisibleTypes & types) != 0; } @Override - public @WindowInsets.Type.InsetsType int getRequestedVisibleTypes() { + public @InsetsType int getRequestedVisibleTypes() { return mRequestedVisibleTypes; } /** * Only the IME can be requested from the EmbeddedWindow. - * @param requestedVisibleTypes other types than {@link WindowInsets.Type.IME} are + * @param requestedVisibleTypes other types than {@link WindowInsets.Type#ime()} are * not sent to system server via WindowlessWindowManager. + * @return an integer as the changed requested visible insets types. */ - void setRequestedVisibleTypes(@WindowInsets.Type.InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @Override diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index cf16204f93a1..f52446ff494c 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -282,7 +282,14 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // TODO(b/353463205) investigate if we should fail the statsToken, or if it's only // temporary null. if (target != null) { - invokeOnImeRequestedChangedListener(target.getWindow(), statsToken); + // If insets target is not available (e.g. RemoteInsetsControlTarget), use current + // IME input target to update IME request state. For example, switch from a task + // with showing IME to a split-screen task without showing IME. + InsetsTarget insetsTarget = target.getWindow(); + if (insetsTarget == null && mServerVisible) { + insetsTarget = mDisplayContent.getImeInputTarget(); + } + invokeOnImeRequestedChangedListener(insetsTarget, statsToken); } } } @@ -314,7 +321,6 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { reportImeDrawnForOrganizerIfNeeded((InsetsControlTarget) caller); } } - changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate(); if (Flags.refactorInsetsController()) { if (changed) { ImeTracker.forLogging().onProgress(statsToken, diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 4bcba13448e9..b4d55a160631 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -387,22 +387,6 @@ class InsetsPolicy { state.addSource(navSource); } return state; - } else if (w.mActivityRecord != null && w.mActivityRecord.mImeInsetsFrozenUntilStartInput) { - // During switching tasks with gestural navigation, before the next IME input target - // starts the input, we should adjust and freeze the last IME visibility of the window - // in case delivering obsoleted IME insets state during transitioning. - final InsetsSource originalImeSource = originalState.peekSource(ID_IME); - - if (originalImeSource != null) { - final boolean imeVisibility = w.isRequestedVisible(Type.ime()); - final InsetsState state = copyState - ? new InsetsState(originalState) - : originalState; - final InsetsSource imeSource = new InsetsSource(originalImeSource); - imeSource.setVisible(imeVisibility); - state.addSource(imeSource); - return state; - } } else if (w.mImeInsetsConsumed) { // Set the IME source (if there is one) to be invisible if it has been consumed. final InsetsSource originalImeSource = originalState.peekSource(ID_IME); @@ -453,9 +437,9 @@ class InsetsPolicy { return originalState; } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { - mStateController.onRequestedVisibleTypesChanged(caller, statsToken); + mStateController.onRequestedVisibleTypesChanged(caller, changedTypes, statsToken); checkAbortTransient(caller); updateBarControlTarget(mFocusedWin); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 9202cf2d5792..164abab992d8 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -219,14 +219,20 @@ class InsetsStateController { } } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { boolean changed = false; for (int i = mProviders.size() - 1; i >= 0; i--) { final InsetsSourceProvider provider = mProviders.valueAt(i); - final boolean isImeProvider = provider.getSource().getType() == WindowInsets.Type.ime(); - changed |= provider.updateClientVisibility(caller, - isImeProvider ? statsToken : null); + final @InsetsType int type = provider.getSource().getType(); + if ((type & changedTypes) != 0) { + final boolean isImeProvider = type == WindowInsets.Type.ime(); + changed |= provider.updateClientVisibility( + caller, isImeProvider ? statsToken : null) + // Fake control target cannot change the client visibility, but it should + // change the insets with its newly requested visibility. + || (caller == provider.getFakeControlTarget()); + } } if (changed) { notifyInsetsChanged(); @@ -435,7 +441,8 @@ class InsetsStateController { for (int i = newControlTargets.size() - 1; i >= 0; i--) { // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here - onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), null /* statsToken */); + onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), + WindowInsets.Type.all(), null /* statsToken */); } newControlTargets.clear(); if (!android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java new file mode 100644 index 000000000000..69463433827f --- /dev/null +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; + +import android.annotation.NonNull; +import android.util.IntArray; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.WmProtoLogGroups; + +/** + * Manages presentation windows. + */ +class PresentationController { + + // TODO(b/395475549): Add support for display add/remove, and activity move across displays. + private final IntArray mPresentingDisplayIds = new IntArray(); + + PresentationController() {} + + private boolean isPresenting(int displayId) { + return mPresentingDisplayIds.contains(displayId); + } + + boolean shouldOccludeActivities(int displayId) { + // All activities on the presenting display must be hidden so that malicious apps can't do + // tap jacking (b/391466268). + // For now, this should only be applied to external displays because presentations can only + // be shown on them. + // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that + // the presentation won't stop its controlling activity. + return enablePresentationForConnectedDisplays() && isPresenting(displayId); + } + + void onPresentationAdded(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (isPresenting(displayId)) { + return; + } + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s", + win.getDisplayId(), win); + mPresentingDisplayIds.add(win.getDisplayId()); + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true); + } + + void onPresentationRemoved(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (!isPresenting(displayId)) { + return; + } + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, + "Presentation removed from display %d: %s", win.getDisplayId(), win); + // TODO(b/393945496): Make sure that there's one presentation at most per display. + final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId); + if (displayIdIndex != -1) { + mPresentingDisplayIds.remove(displayIdIndex); + } + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); + } +} diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 1ad5988e3c2e..8d198b26f396 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -704,9 +704,10 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); } - win.setRequestedVisibleTypes(requestedVisibleTypes); + final @InsetsType int changedTypes = + win.setRequestedVisibleTypes(requestedVisibleTypes); win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win, - imeStatsToken); + changedTypes, imeStatsToken); final Task task = win.getTask(); if (task != null) { task.dispatchTaskInfoChangedIfNeeded(/* forced= */ true); @@ -723,10 +724,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { // TODO(b/353463205) Use different phase here ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); - embeddedWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = embeddedWindow.setRequestedVisibleTypes( requestedVisibleTypes & WindowInsets.Type.ime()); embeddedWindow.getDisplayContent().getInsetsPolicy() - .onRequestedVisibleTypesChanged(embeddedWindow, imeStatsToken); + .onRequestedVisibleTypesChanged( + embeddedWindow, changedTypes, imeStatsToken); } else { ImeTracker.forLogging().onFailed(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index cc14383fc9f9..ae3a015a690d 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -460,7 +460,7 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // If the previous front-most task is moved to the back, then notify of the new // front-most task. - final ActivityRecord topMost = getTopMostActivity(); + final ActivityRecord topMost = getTopNonFinishingActivity(); if (topMost != null) { mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront( topMost.getTask().getTaskInfo()); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 64105f634f84..324852d1a410 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1224,6 +1224,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { false /* ignoringKeyguard */, true /* ignoringInvisibleActivity */); } + @Override ActivityRecord getTopNonFinishingActivity() { return getTopNonFinishingActivity( true /* includeOverlays */, true /* includeLaunchedFromBubble */); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 563bcb771212..25b513d85384 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -555,6 +555,23 @@ class TransitionController { return null; } + /** + * @return The playing transition that is transiently-hiding the given {@param container}, or + * null if there isn't one + * @param container A participant of a transient-hide transition + */ + @Nullable + Transition getTransientHideTransitionForContainer( + @NonNull WindowContainer container) { + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + final Transition transition = mPlayingTransitions.get(i); + if (transition.isInTransientHide(container)) { + return transition; + } + } + return null; + } + /** Returns {@code true} if the display contains a transient-launch transition. */ boolean hasTransientLaunch(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch() diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 225951dbd345..55c2668f62d0 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2079,6 +2079,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getActivity(alwaysTruePredicate(), true /* traverseTopToBottom */); } + ActivityRecord getTopNonFinishingActivity() { + return getActivity(r -> !r.finishing, true /* traverseTopToBottom */); + } + ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) { // Break down into 4 calls to avoid object creation due to capturing input params. if (includeFinishing) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d5626661725e..bb669915e366 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -157,7 +157,6 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.multiCrop; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -503,6 +502,8 @@ public class WindowManagerService extends IWindowManager.Stub final StartingSurfaceController mStartingSurfaceController; + final PresentationController mPresentationController; + private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @Override public void onVrStateChanged(boolean enabled) { @@ -1433,6 +1434,7 @@ public class WindowManagerService extends IWindowManager.Stub setGlobalShadowSettings(); mAnrController = new AnrController(this); mStartingSurfaceController = new StartingSurfaceController(this); + mPresentationController = new PresentationController(); mBlurController = new BlurController(mContext, mPowerManager); mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); @@ -1937,16 +1939,8 @@ public class WindowManagerService extends IWindowManager.Stub } outSizeCompatScale[0] = win.getCompatScaleForClient(); - if (res >= ADD_OKAY - && (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION)) { - displayContent.mIsPresenting = true; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - displayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } - mDisplayManagerInternal.onPresentation(displayContent.getDisplay().getDisplayId(), - /*isShown=*/ true); + if (res >= ADD_OKAY && win.isPresentation()) { + mPresentationController.onPresentationAdded(win); } } @@ -4732,11 +4726,13 @@ public class WindowManagerService extends IWindowManager.Stub } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES); - dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(visibleTypes, mask); + final @InsetsType int changedTypes = + dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes( + visibleTypes, mask); // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here, if null. dc.getInsetsStateController().onRequestedVisibleTypesChanged( - dc.mRemoteInsetsControlTarget, statsToken); + dc.mRemoteInsetsControlTarget, changedTypes, statsToken); } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index a11f4b1f3fc3..924b9de5a562 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -702,9 +702,23 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if ((entry.getValue().getChangeMask() & WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) { - // Disable entering pip (eg. when recents pretends to finish itself) - if (chain.mTransition != null) { - chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */); + if (com.android.wm.shell.Flags.enableRecentsBookendTransition()) { + // If we are using a bookend transition, then the transition that we need + // to disable pip on finish is the original transient transition, not the + // bookend transition + final Transition transientHideTransition = + mTransitionController.getTransientHideTransitionForContainer(wc); + if (transientHideTransition != null) { + transientHideTransition.setCanPipOnFinish(false); + } else { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Set do-not-pip: no task"); + } + } else { + // Disable entering pip (eg. when recents pretends to finish itself) + if (chain.mTransition != null) { + chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */); + } } } // A bit hacky, but we need to detect "remove PiP" so that we can "wrap" the diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 84d8f840d849..589724182980 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -182,7 +182,6 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -822,17 +821,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @VisibleForTesting - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) { - setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); + @InsetsType int setRequestedVisibleTypes( + @InsetsType int requestedVisibleTypes, @InsetsType int mask) { + return setRequestedVisibleTypes( + mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); } /** @@ -2069,38 +2074,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP super.onMovedByResize(); } - void onAppVisibilityChanged(boolean visible, boolean runningAppAnimation) { + void onAppCommitInvisible() { for (int i = mChildren.size() - 1; i >= 0; --i) { - mChildren.get(i).onAppVisibilityChanged(visible, runningAppAnimation); + mChildren.get(i).onAppCommitInvisible(); } - - final boolean isVisibleNow = isVisibleNow(); - if (mAttrs.type == TYPE_APPLICATION_STARTING) { - // Starting window that's exiting will be removed when the animation finishes. - // Mark all relevant flags for that onExitAnimationDone will proceed all the way - // to actually remove it. - if (!visible && isVisibleNow && mActivityRecord.isAnimating(PARENTS | TRANSITION)) { - ProtoLog.d(WM_DEBUG_ANIM, - "Set animatingExit: reason=onAppVisibilityChanged win=%s", this); - mAnimatingExit = true; - mRemoveOnExit = true; - mWindowRemovalAllowed = true; - } - } else if (visible != isVisibleNow) { - // Run exit animation if: - // 1. App visibility and WS visibility are different - // 2. App is not running an animation - // 3. WS is currently visible - if (!runningAppAnimation && isVisibleNow) { - final AccessibilityController accessibilityController = - mWmService.mAccessibilityController; - final int winTransit = TRANSIT_EXIT; - mWinAnimator.applyAnimationLocked(winTransit, false /* isEntrance */); - if (accessibilityController.hasCallbacks()) { - accessibilityController.onWindowTransition(this, winTransit); - } - } - setDisplayLayoutNeeded(); + if (mAttrs.type != TYPE_APPLICATION_STARTING + && mWmService.mAccessibilityController.hasCallbacks() + // It is a change only if App visibility and WS visibility are different. + && isVisible()) { + mWmService.mAccessibilityController.onWindowTransition(this, TRANSIT_EXIT); } } @@ -2317,15 +2299,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int type = mAttrs.type; - if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) { - // TODO(b/393945496): Make sure that there's one presentation at most per display. - dc.mIsPresenting = false; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - dc.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); - } - mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(), - /*isShown=*/ false); + if (isPresentation()) { + mWmService.mPresentationController.onPresentationRemoved(this); } // Check if window provides non decor insets before clearing its provided insets. final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets(); @@ -3354,6 +3329,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } + boolean isPresentation() { + return mAttrs.type == TYPE_PRESENTATION || mAttrs.type == TYPE_PRIVATE_PRESENTATION; + } + private boolean isOnVirtualDisplay() { return getDisplayContent().mDisplay.getType() == Display.TYPE_VIRTUAL; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java index 6e038f9b67a0..ba02122d1dc5 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java @@ -54,17 +54,7 @@ class EnterpriseSpecificIdCalculator { TelephonyManager telephonyService = context.getSystemService(TelephonyManager.class); Preconditions.checkState(telephonyService != null, "Unable to access telephony service"); - String imei; - try { - imei = telephonyService.getImei(0); - } catch (UnsupportedOperationException doesNotSupportGms) { - // Instead of catching the exception, we could check for FEATURE_TELEPHONY_GSM. - // However that runs the risk of changing a device's existing ESID if on these devices - // telephonyService.getImei() actually returns non-null even when the device does not - // declare FEATURE_TELEPHONY_GSM. - imei = null; - } - mImei = imei; + mImei = telephonyService.getImei(0); String meid; try { meid = telephonyService.getMeid(0); diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index f154dbcee21a..09ce263e9b2f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -3962,7 +3962,7 @@ public class DisplayModeDirectorTest { } @Override - public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + public VotesStatsReporter getVotesStatsReporter() { return null; } diff --git a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java index 2e4b97ef7dd2..371b0c926039 100644 --- a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java @@ -26,6 +26,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; +import android.app.PropertyInvalidatedCache; import android.content.Context; import android.multiuser.Flags; import android.os.UserManager; @@ -75,6 +76,8 @@ public class StorageManagerServiceTest { @Before public void setFixtures() { + PropertyInvalidatedCache.disableForTestMode(); + // Called when WatchedUserStates is constructed doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache()); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java index 1e665c2c5c50..317e19abe511 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java @@ -1550,6 +1550,117 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(queue, List.of(closeSystemDialogs1, closeSystemDialogs2)); } + @Test + public void testDeliveryGroupPolicy_sameAction_multiplePolicies() { + // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_GREEN + // package. + final Intent greenPackageChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN)); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "com.example.green/10002", only the most recent one + // gets delivered and the rest get discarded. + final BroadcastOptions optionsMostRecentPolicyForPackageGreen = + BroadcastOptions.makeBasic(); + optionsMostRecentPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN)); + optionsMostRecentPolicyForPackageGreen.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_RED + // package. + final Intent redPackageChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_RED), List.of(PACKAGE_RED)); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "com.example.red/10001", only the most recent one + // gets delivered and the rest get discarded. + final BroadcastOptions optionsMostRecentPolicyForPackageRed = + BroadcastOptions.makeBasic(); + optionsMostRecentPolicyForPackageRed.setDeliveryGroupMatchingKey("package_changed", + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED)); + optionsMostRecentPolicyForPackageRed.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of + // PACKAGE_GREEN package. + final Intent greenPackageComponentsChangedIntent1 = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), + List.of(PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2")); + final Intent greenPackageComponentsChangedIntent2 = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), + List.of(PACKAGE_GREEN + ".comp3")); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "components-com.example.green/10002", merge the extras + // within these broadcasts such that only one broadcast is sent and the rest are + // discarded. Couple of things to note here: + // 1. We are intentionally using a different policy group + // "components-com.example.green/10002" (as opposed to "com.example.green/10002" used + // earlier), because this is corresponding to a change in some particular components, + // rather than a change to the whole package and we want to keep these two types of + // broadcasts independent. + // 2. We are using 'extrasMerger' to indicate how we want the extras to be merged. This + // assumes that broadcasts belonging to the group 'components-com.example.green/10002' + // will have the same values for all the extras, except for the one extra + // 'EXTRA_CHANGED_COMPONENT_NAME_LIST'. So, we explicitly specify how to merge this + // extra by using 'STRATEGY_ARRAY_APPEND' strategy, which basically indicates that + // the extra values which are arrays should be concatenated. + final BundleMerger extrasMerger = new BundleMerger(); + extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + BundleMerger.STRATEGY_ARRAY_APPEND); + final BroadcastOptions optionsMergedPolicyForPackageGreen = BroadcastOptions.makeBasic(); + optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + "components-" + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN)); + optionsMergedPolicyForPackageGreen.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsMergedPolicyForPackageGreen.setDeliveryGroupExtrasMerger(extrasMerger); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of + // PACKAGE_RED package. + final Intent redPackageComponentsChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_RED), + List.of(PACKAGE_RED + ".comp1", PACKAGE_RED + ".comp2")); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "components-com.example.red/10001", merge the extras + // within these broadcasts such that only one broadcast is sent and the rest are + // discarded. + final BroadcastOptions optionsMergedPolicyForPackageRed = BroadcastOptions.makeBasic(); + optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + "components-" + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED)); + optionsMergedPolicyForPackageRed.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsMergedPolicyForPackageRed.setDeliveryGroupExtrasMerger(extrasMerger); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent, + optionsMostRecentPolicyForPackageGreen)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageChangedIntent, + optionsMostRecentPolicyForPackageRed)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent1, + optionsMergedPolicyForPackageGreen)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageComponentsChangedIntent, + optionsMergedPolicyForPackageRed)); + // Since this broadcast has DELIVERY_GROUP_MOST_RECENT policy set, the earlier + // greenPackageChangedIntent broadcast with the same policy will be discarded. + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent, + optionsMostRecentPolicyForPackageGreen)); + // Since this broadcast has DELIVERY_GROUP_MERGED policy set, the earlier + // greenPackageComponentsChangedIntent1 broadcast with the same policy will be merged + // with this one and then will be discarded. + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent2, + optionsMergedPolicyForPackageGreen)); + + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + // The extra EXTRA_CHANGED_COMPONENT_NAME_LIST values from + // greenPackageComponentsChangedIntent1 and + // greenPackageComponentsChangedIntent2 broadcasts would be merged, since + // STRATEGY_ARRAY_APPEND was used for this extra. + final Intent expectedGreenPackageComponentsChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN + ".comp3", + PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2")); + verifyPendingRecords(queue, List.of(redPackageChangedIntent, + redPackageComponentsChangedIntent, greenPackageChangedIntent, + expectedGreenPackageComponentsChangedIntent)); + } + private Pair<Intent, BroadcastOptions> createDropboxBroadcast(String tag, long timestampMs, int droppedCount) { final Intent dropboxEntryAdded = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java index 86bf203771ba..409b114100e7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java @@ -27,6 +27,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; @@ -73,6 +74,7 @@ public class SystemBackupAgentTest { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_systemUser_addsAllHelpers() { UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); when(mUserManagerMock.isProfile()).thenReturn(false); @@ -94,10 +96,12 @@ public class SystemBackupAgentTest { "app_gender", "companion", "system_gender", - "display"); + "display", + "input"); } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_systemUser_slicesDisabled_addsAllNonSlicesHelpers() { UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); when(mUserManagerMock.isProfile()).thenReturn(false); @@ -120,10 +124,12 @@ public class SystemBackupAgentTest { "app_gender", "companion", "system_gender", - "display"); + "display", + "input"); } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_profileUser_addsProfileEligibleHelpers() { UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID); when(mUserManagerMock.isProfile()).thenReturn(true); @@ -143,6 +149,7 @@ public class SystemBackupAgentTest { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_nonSystemUser_addsNonSystemEligibleHelpers() { UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID); when(mUserManagerMock.isProfile()).thenReturn(false); @@ -162,7 +169,8 @@ public class SystemBackupAgentTest { "companion", "app_gender", "system_gender", - "display"); + "display", + "input"); } @Test diff --git a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS index c824c3948e2d..c7c23f081044 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS +++ b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS @@ -1,3 +1,6 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. include /core/java/android/view/accessibility/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index 457fde8d74d0..0227ef1d2dc0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -85,7 +85,7 @@ public class AutoclickControllerTest { public void onMotionEvent_lazyInitClickScheduler() { assertThat(mController.mClickScheduler).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mClickScheduler).isNotNull(); } @@ -94,7 +94,7 @@ public class AutoclickControllerTest { public void onMotionEvent_nonMouseSource_notInitClickScheduler() { assertThat(mController.mClickScheduler).isNull(); - injectFakeNonMouseActionDownEvent(); + injectFakeNonMouseActionHoverMoveEvent(); assertThat(mController.mClickScheduler).isNull(); } @@ -103,7 +103,7 @@ public class AutoclickControllerTest { public void onMotionEvent_lazyInitAutoclickSettingsObserver() { assertThat(mController.mAutoclickSettingsObserver).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickSettingsObserver).isNotNull(); } @@ -113,7 +113,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorScheduler() { assertThat(mController.mAutoclickIndicatorScheduler).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorScheduler).isNotNull(); } @@ -123,7 +123,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOff_notInitAutoclickIndicatorScheduler() { assertThat(mController.mAutoclickIndicatorScheduler).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorScheduler).isNull(); } @@ -133,7 +133,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorView() { assertThat(mController.mAutoclickIndicatorView).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorView).isNotNull(); } @@ -143,7 +143,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOff_notInitAutoclickIndicatorView() { assertThat(mController.mAutoclickIndicatorView).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorView).isNull(); } @@ -153,7 +153,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOn_lazyInitAutoclickTypePanelView() { assertThat(mController.mAutoclickTypePanel).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickTypePanel).isNotNull(); } @@ -163,7 +163,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOff_notInitAutoclickTypePanelView() { assertThat(mController.mAutoclickTypePanel).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickTypePanel).isNull(); } @@ -171,7 +171,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onMotionEvent_flagOn_addAutoclickIndicatorViewToWindowManager() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); verify(mMockWindowManager).addView(eq(mController.mAutoclickIndicatorView), any()); } @@ -179,7 +179,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onDestroy_flagOn_removeAutoclickIndicatorViewToWindowManager() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); @@ -189,7 +189,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onDestroy_flagOn_removeAutoclickTypePanelViewToWindowManager() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); mController.mAutoclickTypePanel = mockAutoclickTypePanel; @@ -200,7 +200,7 @@ public class AutoclickControllerTest { @Test public void onMotionEvent_initClickSchedulerDelayFromSetting() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); int delay = Settings.Secure.getIntForUser( @@ -214,7 +214,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onMotionEvent_flagOn_initCursorAreaSizeFromSetting() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); int size = Settings.Secure.getIntForUser( @@ -238,7 +238,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onKeyEvent_modifierKey_updateMetaStateWhenControllerNotNull() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); int metaState = KeyEvent.META_ALT_ON | KeyEvent.META_META_ON; injectFakeKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, metaState); @@ -250,7 +250,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onKeyEvent_modifierKey_cancelAutoClickWhenAdditionalRegularKeyPresssed() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); injectFakeKeyEvent(KeyEvent.KEYCODE_J, KeyEvent.META_ALT_ON); @@ -260,7 +260,7 @@ public class AutoclickControllerTest { @Test public void onDestroy_clearClickScheduler() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); @@ -269,7 +269,7 @@ public class AutoclickControllerTest { @Test public void onDestroy_clearAutoclickSettingsObserver() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); @@ -279,21 +279,61 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onDestroy_flagOn_clearAutoclickIndicatorScheduler() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); assertThat(mController.mAutoclickIndicatorScheduler).isNull(); } - private void injectFakeMouseActionDownEvent() { - MotionEvent event = getFakeMotionDownEvent(); + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_hoverEnter_doesNotScheduleClick() { + injectFakeMouseActionHoverMoveEvent(); + + // Send hover enter event. + MotionEvent hoverEnter = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_ENTER, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverEnter.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0); + + // Verify there is no pending click. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_hoverMove_scheduleClick() { + injectFakeMouseActionHoverMoveEvent(); + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + + // Verify there is a pending click. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); + } + + private void injectFakeMouseActionHoverMoveEvent() { + MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); mController.onMotionEvent(event, event, /* policyFlags= */ 0); } - private void injectFakeNonMouseActionDownEvent() { - MotionEvent event = getFakeMotionDownEvent(); + private void injectFakeNonMouseActionHoverMoveEvent() { + MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_KEYBOARD); mController.onMotionEvent(event, event, /* policyFlags= */ 0); } @@ -309,11 +349,11 @@ public class AutoclickControllerTest { mController.onKeyEvent(keyEvent, /* policyFlags= */ 0); } - private MotionEvent getFakeMotionDownEvent() { + private MotionEvent getFakeMotionHoverMoveEvent() { return MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, - /* action= */ MotionEvent.ACTION_DOWN, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, /* x= */ 0, /* y= */ 0, /* metaState= */ 0); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS new file mode 100644 index 000000000000..9592bfdfa73b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS @@ -0,0 +1,6 @@ +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 770744. + +include /services/accessibility/java/com/android/server/accessibility/magnification/OWNERS diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java index 770712a191fd..41011928f8b3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java @@ -204,7 +204,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { .build()); final Notification n = new Notification.Builder(getContext()) .setContentTitle("foo") - .setCategory(CATEGORY_ALARM) + .setCategory(new String("alarm")) .setSmallIcon(android.R.drawable.sym_def_app_icon) .build(); NotificationRecord r = getRecord(channel, n); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 65150e7b48fc..440f43e9b926 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -49,17 +49,13 @@ import static android.content.res.Configuration.UI_MODE_TYPE_DESK; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.Process.NOBODY_UID; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.InsetsSource.ID_IME; -import static android.view.WindowInsets.Type.ime; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; -import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_PIP; @@ -125,7 +121,6 @@ import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.ClientTransactionItem; import android.app.servertransaction.DestroyActivityItem; import android.app.servertransaction.PauseActivityItem; -import android.app.servertransaction.WindowStateResizeItem; import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.Intent; @@ -149,8 +144,6 @@ import android.view.DisplayInfo; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner.Stub; import android.view.IWindowManager; -import android.view.InsetsSource; -import android.view.InsetsState; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.Surface; @@ -171,7 +164,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import java.util.ArrayList; @@ -3370,178 +3362,6 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); } - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() { - final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); - makeWindowVisibleAndDrawn(app, mImeWindow); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.setImeInputTarget(app); - - // Simulate app is closing and expect the last IME is shown and IME insets is frozen. - mDisplayContent.mOpeningApps.clear(); - app.mActivityRecord.commitVisibility(false, false); - app.mActivityRecord.onWindowsGone(); - - assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Expect IME insets frozen state will reset when the activity has no IME focusable window. - app.mActivityRecord.forAllWindows(w -> { - w.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; - return true; - }, true); - - app.mActivityRecord.commitVisibility(true, false); - app.mActivityRecord.onWindowsVisible(); - - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - } - - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() { - final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); - - mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( - mImeWindow, null, null); - mImeWindow.getControllableInsetProvider().setServerVisible(true); - - InsetsSource imeSource = new InsetsSource(ID_IME, ime()); - app.mAboveInsetsState.addSource(imeSource); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.updateImeInputAndControlTarget(app); - - InsetsState state = app.getInsetsState(); - assertFalse(state.getOrCreateSource(imeSource.getId(), ime()).isVisible()); - assertTrue(state.getOrCreateSource(imeSource.getId(), ime()).getFrame().isEmpty()); - - // Simulate app is closing and expect IME insets is frozen. - mDisplayContent.mOpeningApps.clear(); - app.mActivityRecord.commitVisibility(false, false); - app.mActivityRecord.onWindowsGone(); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Simulate app re-start input or turning screen off/on then unlocked by un-secure - // keyguard to back to the app, expect IME insets is not frozen - app.mActivityRecord.commitVisibility(true, false); - mDisplayContent.updateImeInputAndControlTarget(app); - performSurfacePlacementAndWaitForWindowAnimator(); - - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - imeSource.setVisible(true); - imeSource.setFrame(new Rect(100, 400, 500, 500)); - app.mAboveInsetsState.addSource(imeSource); - - // Verify when IME is visible and the app can receive the right IME insets from policy. - makeWindowVisibleAndDrawn(app, mImeWindow); - state = app.getInsetsState(); - assertTrue(state.peekSource(ID_IME).isVisible()); - assertEquals(state.peekSource(ID_IME).getFrame(), imeSource.getFrame()); - } - - @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) - @Test - public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest() - throws RemoteException { - final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build(); - final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build(); - - mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( - mImeWindow, null, null); - mImeWindow.getControllableInsetProvider().setServerVisible(true); - - // Simulate app2 is closing and let app1 is visible to be IME targets. - makeWindowVisibleAndDrawn(app1, mImeWindow); - mDisplayContent.setImeLayeringTarget(app1); - mDisplayContent.updateImeInputAndControlTarget(app1); - app2.mActivityRecord.commitVisibility(false, false); - - // app1 requests IME visible. - app1.setRequestedVisibleTypes(ime(), ime()); - mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1, - null /* statsToken */); - - // Verify app1's IME insets is visible and app2's IME insets frozen flag set. - assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); - assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Simulate switching to app2 to make it visible to be IME targets. - spyOn(app2); - spyOn(app2.mClient); - spyOn(app2.getProcess()); - ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class); - doReturn(true).when(app2).isReadyToDispatchInsetsState(); - mDisplayContent.setImeLayeringTarget(app2); - app2.mActivityRecord.commitVisibility(true, false); - mDisplayContent.updateImeInputAndControlTarget(app2); - performSurfacePlacementAndWaitForWindowAnimator(); - - // Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets - // to client if the app didn't request IME visible. - assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem( - isA(WindowStateResizeItem.class)); - assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); - } - - @Test - public void testImeInsetsFrozenFlag_multiWindowActivities() { - final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent); - final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken( - imeToken).build(); - makeWindowVisibleAndDrawn(ime); - - // Create a split-screen root task with activity1 and activity 2. - final Task task = new TaskBuilder(mSupervisor) - .setCreateParentTask(true).setCreateActivity(true).build(); - task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - final ActivityRecord activity1 = task.getTopNonFinishingActivity(); - activity1.getTask().setResumedActivity(activity1, "testApp1"); - - final ActivityRecord activity2 = new TaskBuilder(mSupervisor) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) - .setCreateActivity(true).build().getTopMostActivity(); - activity2.getTask().setResumedActivity(activity2, "testApp2"); - activity2.getTask().setParent(task.getRootTask()); - - // Simulate activity1 and activity2 both have set mImeInsetsFrozenUntilStartInput when - // invisible to user. - activity1.mImeInsetsFrozenUntilStartInput = true; - activity2.mImeInsetsFrozenUntilStartInput = true; - - final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken( - activity1).build(); - final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowToken( - activity2).build(); - makeWindowVisibleAndDrawn(app1, app2); - - final InsetsStateController controller = mDisplayContent.getInsetsStateController(); - controller.getImeSourceProvider().setWindowContainer( - ime, null, null); - ime.getControllableInsetProvider().setServerVisible(true); - - // app1 starts input and expect IME insets for all activities in split-screen will be - // frozen until the input started. - mDisplayContent.setImeLayeringTarget(app1); - mDisplayContent.updateImeInputAndControlTarget(app1); - mDisplayContent.computeImeTarget(true /* updateImeTarget */); - performSurfacePlacementAndWaitForWindowAnimator(); - - assertEquals(app1, mDisplayContent.getImeInputTarget()); - assertFalse(activity1.mImeInsetsFrozenUntilStartInput); - assertFalse(activity2.mImeInsetsFrozenUntilStartInput); - - app1.setRequestedVisibleTypes(ime()); - controller.onRequestedVisibleTypesChanged(app1, null /* statsToken */); - - // Expect all activities in split-screen will get IME insets visible state - assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); - assertTrue(app2.getInsetsState().peekSource(ID_IME).isVisible()); - } - @Test public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() { final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java index e0b700a4ffe3..eaffc481098e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -97,6 +97,7 @@ public class DesktopModeHelperTest { public void canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceCheck_returnsFalse() throws Exception { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); doReturn(true).when(mMockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported)); disableEnforceDeviceRestriction(); @@ -148,6 +149,7 @@ public class DesktopModeHelperTest { @Test public void canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue(); } @@ -176,21 +178,21 @@ public class DesktopModeHelperTest { @Test public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { - doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); } @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @@ -200,7 +202,7 @@ public class DesktopModeHelperTest { eq(R.bool.config_isDesktopModeDevOptionSupported) ); - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); } private void resetEnforceDeviceRestriction() throws Exception { @@ -234,4 +236,4 @@ public class DesktopModeHelperTest { Settings.Global.putInt(mContext.getContentResolver(), DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting()); } -} +}
\ No newline at end of file diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index fdde3b38f19f..d305c2f54456 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -1345,7 +1345,7 @@ public class DesktopModeLaunchParamsModifierTests extends private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported, boolean enforceDeviceRestrictions) { doReturn(isDesktopModeSupported) - .when(() -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any())); + .when(() -> DesktopModeHelper.canEnterDesktopMode(any())); doReturn(enforceDeviceRestrictions) .when(DesktopModeHelper::shouldEnforceDeviceRestrictions); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java index 285a5e246e0c..ea21bb34597d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java @@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.any; /** Robot for changing desktop windowing properties. */ class DesktopWindowingRobot { void allowEnterDesktopMode(boolean isAllowed) { - doReturn(isAllowed).when(() -> DesktopModeHelper.canEnterDesktopMode(any())); + doReturn(isAllowed).when(() -> + DesktopModeHelper.canEnterDesktopMode(any())); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 6c5fe1d8551e..71e34ef220d3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -53,6 +53,7 @@ import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -400,9 +401,9 @@ public class InsetsPolicyTest extends WindowTestsBase { assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars())); assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars())); - mAppWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = mAppWindow.setRequestedVisibleTypes( navigationBars() | statusBars(), navigationBars() | statusBars()); - policy.onRequestedVisibleTypesChanged(mAppWindow, null /* statsToken */); + policy.onRequestedVisibleTypesChanged(mAppWindow, changedTypes, null /* statsToken */); waitUntilWindowAnimatorIdle(); controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 973c8d0a8464..5525bae89138 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -52,6 +52,7 @@ import android.util.SparseArray; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -201,8 +202,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); getController().onImeControlTargetChanged(base); - base.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(base, null /* statsToken */); + final @InsetsType int changedTypes = base.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(base, changedTypes, null /* statsToken */); if (android.view.inputmethod.Flags.refactorInsetsController()) { // to set the serverVisibility, the IME needs to be drawn and onPostLayout be called. mImeWindow.mWinAnimator.mDrawState = HAS_DRAWN; @@ -509,8 +510,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(app); mDisplayContent.updateImeInputAndControlTarget(app); - app.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(app, null /* statsToken */); + final @InsetsType int changedTypes = app.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(app, changedTypes, null /* statsToken */); assertTrue(ime.getControllableInsetProvider().getSource().isVisible()); if (android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java new file mode 100644 index 000000000000..db90c28ec7df --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.Display.FLAG_PRESENTATION; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; + +import android.graphics.Rect; +import android.os.UserHandle; +import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; +import android.view.IWindow; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest WmTests:PresentationControllerTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class PresentationControllerTests extends WindowTestsBase { + + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationHidesActivitiesBehind() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.flags = FLAG_PRESENTATION; + final DisplayContent dc = createNewDisplay(displayInfo); + final int displayId = dc.getDisplayId(); + doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + final ActivityRecord activity = createActivityRecord(createTask(dc)); + assertTrue(activity.isVisible()); + + doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); + final int uid = 100000; // uid for non-system user + final Session session = createTestSession(mAtm, 1234 /* pid */, uid); + final int userId = UserHandle.getUserId(uid); + doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_PRESENTATION); + + final IWindow clientWindow = new TestIWindow(); + final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, + userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), + new InsetsSourceControl.Array(), new Rect(), new float[1]); + assertTrue(result >= WindowManagerGlobal.ADD_OKAY); + assertFalse(activity.isVisible()); + + final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); + window.removeImmediately(); + assertTrue(activity.isVisible()); + } +} 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 1323d8a59cef..71e84c0f1821 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -26,7 +26,6 @@ import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCRE import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_OWN_FOCUS; -import static android.view.Display.FLAG_PRESENTATION; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; @@ -55,7 +54,6 @@ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; -import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; import static com.google.common.truth.Truth.assertThat; @@ -102,7 +100,6 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; -import android.view.DisplayInfo; import android.view.IWindow; import android.view.InputChannel; import android.view.InputDevice; @@ -1409,38 +1406,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { assertEquals(activityWindowInfo2, activityWindowInfo3); } - @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) - @Test - public void testPresentationHidesActivitiesBehind() { - DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.copyFrom(mDisplayInfo); - displayInfo.flags = FLAG_PRESENTATION; - DisplayContent dc = createNewDisplay(displayInfo); - int displayId = dc.getDisplayId(); - doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); - ActivityRecord activity = createActivityRecord(createTask(dc)); - assertTrue(activity.isVisible()); - - doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); - int uid = 100000; // uid for non-system user - Session session = createTestSession(mAtm, 1234 /* pid */, uid); - int userId = UserHandle.getUserId(uid); - doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); - WindowManager.LayoutParams params = new WindowManager.LayoutParams( - LayoutParams.TYPE_PRESENTATION); - - final IWindow clientWindow = new TestIWindow(); - int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, - userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), - new InsetsSourceControl.Array(), new Rect(), new float[1]); - assertTrue(result >= WindowManagerGlobal.ADD_OKAY); - assertFalse(activity.isVisible()); - - final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); - window.removeImmediately(); - assertTrue(activity.isVisible()); - } - @Test public void testAddOverlayWindowToUnassignedDisplay_notAllowed_ForVisibleBackgroundUsers() { doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index cff172f55601..a718c06cc2fa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -1282,7 +1282,6 @@ public class WindowStateTests extends WindowTestsBase { // Simulate app plays closing transition to app2. app.mActivityRecord.commitVisibility(false, false); assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Verify the IME insets is visible on app, but not for app2 during app task switching. assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); @@ -1305,7 +1304,7 @@ public class WindowStateTests extends WindowTestsBase { // Simulate app2 in multi-window mode is going to background to switch to the fullscreen // app which requests IME with updating all windows Insets State when IME is above app. - app2.mActivityRecord.mImeInsetsFrozenUntilStartInput = true; + app2.mActivityRecord.setVisibleRequested(false); mDisplayContent.setImeLayeringTarget(app); mDisplayContent.setImeInputTarget(app); app.setRequestedVisibleTypes(ime(), ime()); @@ -1324,7 +1323,6 @@ public class WindowStateTests extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(app2); app.mActivityRecord.commitVisibility(false, false); assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Verify the IME insets is still visible on app, but not for app2 during task switching. assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index e0af22369182..d2741ac7ee9f 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -4821,10 +4821,14 @@ public class SubscriptionManager { + "Invalid subscriptionId: " + subscriptionId); } + String contextPkg = mContext != null ? mContext.getOpPackageName() : "<unknown>"; + String contextAttributionTag = mContext != null ? mContext.getAttributionTag() : null; + try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId); + return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId, contextPkg, + contextAttributionTag); } else { throw new IllegalStateException("subscription service unavailable."); } diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 1bfec29a3cf4..a974c615a4ae 100644 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -347,13 +347,17 @@ interface ISub { * Returns whether the given subscription is associated with the calling user. * * @param subscriptionId the subscription ID of the subscription + * @param callingPackage The package maing the call + * @param callingFeatureId The feature in the package + * @return {@code true} if the subscription is associated with the user that the current process * is running in; {@code false} otherwise. * * @throws IllegalArgumentException if subscription doesn't exist. * @throws SecurityException if the caller doesn't have permissions required. */ - boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId); + boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId, String callingPackage, + String callingFeatureId); /** * Check if subscription and user are associated with each other. diff --git a/tests/AttestationVerificationTest/AndroidManifest.xml b/tests/AttestationVerificationTest/AndroidManifest.xml index 37321ad80b0f..758852bb1074 100644 --- a/tests/AttestationVerificationTest/AndroidManifest.xml +++ b/tests/AttestationVerificationTest/AndroidManifest.xml @@ -18,7 +18,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.security.attestationverification"> - <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" /> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" /> <uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> <application> diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json new file mode 100644 index 000000000000..2a3ba5ebde7d --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json @@ -0,0 +1,12 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json new file mode 100644 index 000000000000..e22a834a92bf --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json @@ -0,0 +1,16 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "353017e73dc205a73a9c3de142230370" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java new file mode 100644 index 000000000000..c38517ace5e6 --- /dev/null +++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2025 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.security; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.os.SystemClock; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class CertificateRevocationStatusManagerTest { + + private static final String TEST_CERTIFICATE_FILE_1 = "test_attestation_with_root_certs.pem"; + private static final String TEST_CERTIFICATE_FILE_2 = "test_attestation_wrong_root_certs.pem"; + private static final String TEST_REVOCATION_LIST_FILE_NAME = "test_revocation_list.json"; + private static final String REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_no_test_certs.json"; + private static final String REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_with_test_certs.json"; + private static final String TEST_REVOCATION_STATUS_FILE_NAME = "test_revocation_status.txt"; + private static final String FILE_URL_PREFIX = "file://"; + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + private CertificateFactory mFactory; + private List<X509Certificate> mCertificates1; + private List<X509Certificate> mCertificates2; + private File mRevocationListFile; + private String mRevocationListUrl; + private String mNonExistentRevocationListUrl; + private File mRevocationStatusFile; + private CertificateRevocationStatusManager mCertificateRevocationStatusManager; + + @Before + public void setUp() throws Exception { + mFactory = CertificateFactory.getInstance("X.509"); + mCertificates1 = getCertificateChain(TEST_CERTIFICATE_FILE_1); + mCertificates2 = getCertificateChain(TEST_CERTIFICATE_FILE_2); + mRevocationListFile = new File(mContext.getFilesDir(), TEST_REVOCATION_LIST_FILE_NAME); + mRevocationListUrl = FILE_URL_PREFIX + mRevocationListFile.getAbsolutePath(); + File noSuchFile = new File(mContext.getFilesDir(), "file_does_not_exist"); + mNonExistentRevocationListUrl = FILE_URL_PREFIX + noSuchFile.getAbsolutePath(); + mRevocationStatusFile = new File(mContext.getFilesDir(), TEST_REVOCATION_STATUS_FILE_NAME); + } + + @After + public void tearDown() throws Exception { + mRevocationListFile.delete(); + mRevocationStatusFile.delete(); + } + + @Test + public void checkRevocationStatus_doesNotExistOnRemoteRevocationList_noException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_existsOnRemoteRevocationList_throwsException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void + checkRevocationStatus_cannotReachRemoteRevocationList_noStoredStatus_throwsException() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_savesRevocationStatus() throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + + assertThat(mRevocationStatusFile.length()).isGreaterThan(0); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_certsSaved_noException() + throws Exception { + // call checkRevocationStatus once to save the revocation status + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // call checkRevocationStatus again with mNonExistentRevocationListUrl + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_someCertsNotSaved_exception() + throws Exception { + // call checkRevocationStatus once to save the revocation status for mCertificates2 + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates2); + // call checkRevocationStatus again with mNonExistentRevocationListUrl, this time for + // mCertificates1 + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_someCertsStatusTooOld_exception() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expiredStatusDate = + now.minusDays(CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK + 1); + Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>(); + lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(0)), expiredStatusDate); + for (int i = 1; i < mCertificates1.size(); i++) { + lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(i)), now); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_allCertResultsFresh_noException() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + LocalDateTime bearlyNotExpiredStatusDate = + LocalDateTime.now() + .minusDays( + CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK - 1); + Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>(); + for (X509Certificate certificate : mCertificates1) { + lastRevocationCheckData.put(getSerialNumber(certificate), bearlyNotExpiredStatusDate); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void updateLastRevocationCheckData_correctlySavesStatus() throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (X509Certificate certificate : mCertificates1) { + areCertificatesRevoked.put(getSerialNumber(certificate), false); + } + + mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked); + + // no exception + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // revoke one certificate and try again + areCertificatesRevoked.put(getSerialNumber(mCertificates1.getLast()), true); + mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked); + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void updateLastRevocationCheckDataForAllPreviouslySeenCertificates_updatesCorrectly() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + // populate the revocation status file + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // Sleep for 2 second so that the current time changes + SystemClock.sleep(2000); + LocalDateTime timestampBeforeUpdate = LocalDateTime.now(); + JSONObject revocationList = mCertificateRevocationStatusManager.fetchRemoteRevocationList(); + List<String> otherCertificatesToCheck = new ArrayList<>(); + String serialNumber1 = "1234567"; // not revoked + String serialNumber2 = "8350192447815228107"; // revoked + String serialNumber3 = "987654"; // not revoked + otherCertificatesToCheck.add(serialNumber1); + otherCertificatesToCheck.add(serialNumber2); + otherCertificatesToCheck.add(serialNumber3); + + mCertificateRevocationStatusManager + .updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + revocationList, otherCertificatesToCheck); + + Map<String, LocalDateTime> lastRevocationCheckData = + mCertificateRevocationStatusManager.getLastRevocationCheckData(); + assertThat(lastRevocationCheckData.get(serialNumber1)).isAtLeast(timestampBeforeUpdate); + assertThat(lastRevocationCheckData).doesNotContainKey(serialNumber2); // revoked + assertThat(lastRevocationCheckData.get(serialNumber3)).isAtLeast(timestampBeforeUpdate); + // validate that the existing certificates on the file got updated too + for (X509Certificate certificate : mCertificates1) { + assertThat(lastRevocationCheckData.get(getSerialNumber(certificate))) + .isAtLeast(timestampBeforeUpdate); + } + } + + private List<X509Certificate> getCertificateChain(String fileName) throws Exception { + Collection<? extends Certificate> certificates = + mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName)); + ArrayList<X509Certificate> x509Certs = new ArrayList<>(); + for (Certificate cert : certificates) { + x509Certs.add((X509Certificate) cert); + } + return x509Certs; + } + + private void copyFromAssetToFile(String assetFileName, File targetFile) throws Exception { + byte[] data; + try (InputStream in = mContext.getResources().getAssets().open(assetFileName)) { + data = in.readAllBytes(); + } + try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) { + fileOutputStream.write(data); + } + } + + private String getSerialNumber(X509Certificate certificate) { + return certificate.getSerialNumber().toString(16); + } +} diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index 12670cda74b2..ac704e5e7c39 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -52,10 +52,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 481a8bb66fee..1b2007deae27 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -50,10 +50,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 37bdf6b8614d..de47f013271a 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -1438,6 +1438,58 @@ class KeyGestureControllerTests { ) } + @Test + @Parameters(method = "customInputGesturesTestArguments") + fun testCustomKeyGestureRestoredFromBackup(test: TestData) { + val userId = 10 + setupKeyGestureController() + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) + ) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build() + + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + val backupData = keyGestureController.getInputGestureBackupPayload(userId) + + // Delete the old data and reinitialize the controller simulating a "fresh" install. + tempFile.delete() + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + + // Initially there should be no gestures registered. + var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 0, + savedInputGestures.size + ) + + // After the restore, there should be the original gesture re-registered. + keyGestureController.applyInputGesturesBackupPayload(backupData, userId) + savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[0]) + ) + } + class TouchpadTestData( val name: String, val touchpadGestureType: Int, @@ -1549,6 +1601,53 @@ class KeyGestureControllerTests { ) } + + @Test + @Parameters(method = "customTouchpadGesturesTestArguments") + fun testCustomTouchpadGesturesRestoredFromBackup(test: TouchpadTestData) { + val userId = 10 + setupKeyGestureController() + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + val backupData = keyGestureController.getInputGestureBackupPayload(userId) + + // Delete the old data and reinitialize the controller simulating a "fresh" install. + tempFile.delete() + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + + // Initially there should be no gestures registered. + var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 0, + savedInputGestures.size + ) + + // After the restore, there should be the original gesture re-registered. + keyGestureController.applyInputGesturesBackupPayload(backupData, userId) + savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[0]) + ) + } + private fun testKeyGestureInternal(test: TestData) { val handledEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index f00a6cad6b46..20315561cceb 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -54,9 +54,7 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); - } + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); return true; }; @@ -67,9 +65,7 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); - } + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); return true; }; @@ -80,9 +76,7 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); - } + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); return true; }; @@ -93,9 +87,7 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); - } + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); return true; }; @@ -106,9 +98,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - if (value) { - value->emplace(arg); - } + value->emplace(arg); return true; }; @@ -118,9 +108,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - if (value) { - *value = true; - } + *value = true; return true; }; diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index ad167c979662..2a3cb2a0c65d 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -159,22 +159,4 @@ TEST(CommandTest, ShortOptions) { ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); } -TEST(CommandTest, OptionsWithNullptrToAcceptValues) { - TestCommand command; - command.AddRequiredFlag("--rflag", "", nullptr); - command.AddRequiredFlagList("--rlflag", "", nullptr); - command.AddOptionalFlag("--oflag", "", nullptr); - command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); - command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); - command.AddOptionalSwitch("--switch", "", nullptr); - - ASSERT_EQ(0, command.Execute({ - "--rflag"s, "1"s, - "--rlflag"s, "1"s, - "--oflag"s, "1"s, - "--olflag"s, "1"s, - "--olflag2"s, "1"s, - "--switch"s}, &std::cerr)); -} - } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 060bc5fa2242..6c3eae11eab9 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,6 +425,9 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } + if (enable_sparse_encoding_) { + table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; + } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 98c8f5ff89c0..9452e588953e 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,9 +36,11 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " + "the APK is O+", + &enable_sparse_encoding_); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -85,6 +87,7 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; + bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 4718fbf085f8..ff4d8ef2ec25 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2505,6 +2505,9 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } + if (options_.use_sparse_encoding) { + options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; + } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index b5bd905c02be..2f17853718ec 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,6 +75,7 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; + bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -162,11 +163,9 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch( - "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + AddOptionalSwitch("--enable-sparse-encoding", + "This decreases APK size at the cost of resource retrieval performance.", + &options_.use_sparse_encoding); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index f218307af578..762441ee1872 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,6 +406,9 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } + if (options_.enable_sparse_encoding) { + options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; + } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index e3af584cbbd9..012b0f230ca2 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,6 +61,9 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; + // Whether sparse encoding should be used for O+ resources. + bool enable_sparse_encoding = false; + // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -103,9 +106,11 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " + "the APK is O+", + &options_.enable_sparse_encoding); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index b8ac7925d44e..1a82021bce71 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index f1c4c3512ed3..0633bc81cb25 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,7 +37,8 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). + // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less + // than O only applies sparse encoding for resource configuration available on O+. Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -46,7 +47,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index e3d589eb078b..0f1168514c4a 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Disabled; + options.sparse_entries = SparseEntriesMode::Enabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Disabled; + options.sparse_entries = SparseEntriesMode::Enabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 664d8412a3be..5c3dfdcadfec 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -3,8 +3,6 @@ ## Version 2.20 - Too many features, bug fixes, and improvements to list since the last minor version update in 2017. This README will be updated more frequently in the future. -- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The - `--enable-sparse-encoding` flag still exists, but is a no-op. ## Version 2.19 - Added navigation resource type. |