diff options
74 files changed, 1225 insertions, 186 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 13e12101b3d5..e03767cafd9e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -34131,6 +34131,7 @@ package android.os { method public android.os.StrictMode.VmPolicy build(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectActivityLeaks(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectAll(); + method @FlaggedApi("com.android.window.flags.bal_strict_mode") @NonNull public android.os.StrictMode.VmPolicy.Builder detectBlockedBackgroundActivityLaunch(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectCleartextNetwork(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectContentUriWithoutPermission(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectCredentialProtectedWhileLocked(); @@ -34143,6 +34144,7 @@ package android.os { method @NonNull public android.os.StrictMode.VmPolicy.Builder detectNonSdkApiUsage(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectUnsafeIntentLaunch(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectUntaggedSockets(); + method @FlaggedApi("com.android.window.flags.bal_strict_mode") @NonNull public android.os.StrictMode.VmPolicy.Builder ignoreBlockedBackgroundActivityLaunch(); method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeath(); method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeathOnCleartextNetwork(); method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeathOnFileUriExposure(); diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 0a05144254ab..003104ab2b0b 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -359,6 +359,23 @@ interface IActivityTaskManager { in RemoteCallback navigationObserver, in BackAnimationAdapter adaptor); /** + * registers a callback to be invoked when a background activity launch is aborted. + * + * @param observer callback to be registered. + * @return true if the callback was successfully registered, false otherwise. + * @hide + */ + boolean registerBackgroundActivityStartCallback(in IBinder binder); + + /** + * unregisters a callback to be invoked when a background activity launch is aborted. + * + * @param observer callback to be registered. + * @hide + */ + void unregisterBackgroundActivityStartCallback(in IBinder binder); + + /** * registers a callback to be invoked when the screen is captured. * * @param observer callback to be registered. diff --git a/core/java/android/app/IBackgroundActivityLaunchCallback.aidl b/core/java/android/app/IBackgroundActivityLaunchCallback.aidl new file mode 100644 index 000000000000..6dfb5182486a --- /dev/null +++ b/core/java/android/app/IBackgroundActivityLaunchCallback.aidl @@ -0,0 +1,26 @@ +/* +* 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 android.app; + +/** + * Callback to find out when a background activity launch is aborted. + * @hide + */ +oneway interface IBackgroundActivityLaunchCallback +{ + void onBackgroundActivityLaunchAborted(in String message); +} diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index bc9e709420f1..62ae6ac951be 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -70,6 +70,7 @@ import java.util.concurrent.atomic.AtomicLong; * @hide */ @TestApi +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class PropertyInvalidatedCache<Query, Result> { /** * This is a configuration class that customizes a cache instance. @@ -798,9 +799,17 @@ public class PropertyInvalidatedCache<Query, Result> { = new ConcurrentHashMap<>(); // True if shared memory is flag-enabled, false otherwise. Read the flags exactly once. - private static final boolean sSharedMemoryAvailable = - com.android.internal.os.Flags.applicationSharedMemoryEnabled() - && android.app.Flags.picUsesSharedMemory(); + private static final boolean sSharedMemoryAvailable = isSharedMemoryAvailable(); + + @android.ravenwood.annotation.RavenwoodReplace + private static boolean isSharedMemoryAvailable() { + return com.android.internal.os.Flags.applicationSharedMemoryEnabled() + && android.app.Flags.picUsesSharedMemory(); + } + + private static boolean isSharedMemoryAvailable$ravenwood() { + return false; // Always disable shared memory on Ravenwood. (for now) + } // Return true if this cache can use shared memory for its nonce. Shared memory may be used // if the module is the system. @@ -1787,14 +1796,6 @@ public class PropertyInvalidatedCache<Query, Result> { // block. private static final int MAX_STRING_LENGTH = 63; - // The raw byte block. Strings are stored as run-length encoded byte arrays. The first - // byte is the length of the following string. It is an axiom of the system that the - // string block is initially all zeros and that it is write-once memory: new strings are - // appended to existing strings, so there is never a need to revisit strings that have - // already been pulled from the string block. - @GuardedBy("mLock") - private final byte[] mStringBlock; - // The expected hash code of the string block. If the hash over the string block equals // this value, then the string block is valid. Otherwise, the block is not valid and // should be re-read. An invalid block generally means that a client has read the shared @@ -1806,12 +1807,15 @@ public class PropertyInvalidatedCache<Query, Result> { // logging. private final int mMaxNonce; + // The size of the native byte block. + private final int mMaxByte; + /** @hide */ @VisibleForTesting public NonceStore(long ptr, boolean mutable) { mPtr = ptr; mMutable = mutable; - mStringBlock = new byte[nativeGetMaxByte(ptr)]; + mMaxByte = nativeGetMaxByte(ptr); mMaxNonce = nativeGetMaxNonce(ptr); refreshStringBlockLocked(); } @@ -1870,17 +1874,17 @@ public class PropertyInvalidatedCache<Query, Result> { // and the block hash is not checked. The function skips past strings that have already // been read, and then processes any new strings. @GuardedBy("mLock") - private void updateStringMapLocked() { + private void updateStringMapLocked(byte[] block) { int index = 0; int offset = 0; - while (offset < mStringBlock.length && mStringBlock[offset] != 0) { + while (offset < block.length && block[offset] != 0) { if (index > mHighestIndex) { // Only record the string if it has not been seen yet. - final String s = new String(mStringBlock, offset+1, mStringBlock[offset]); + final String s = new String(block, offset+1, block[offset]); mStringHandle.put(s, index); mHighestIndex = index; } - offset += mStringBlock[offset] + 1; + offset += block[offset] + 1; index++; } mStringBytes = offset; @@ -1889,24 +1893,21 @@ public class PropertyInvalidatedCache<Query, Result> { // Append a string to the string block and update the hash. This does not write the block // to shared memory. @GuardedBy("mLock") - private void appendStringToMapLocked(@NonNull String str) { + private void appendStringToMapLocked(@NonNull String str, @NonNull byte[] block) { int offset = 0; - while (offset < mStringBlock.length && mStringBlock[offset] != 0) { - offset += mStringBlock[offset] + 1; + while (offset < block.length && block[offset] != 0) { + offset += block[offset] + 1; } final byte[] strBytes = str.getBytes(); - if (offset + strBytes.length >= mStringBlock.length) { + if (offset + strBytes.length >= block.length) { // Overflow. Do not add the string to the block; the string will remain undefined. return; } - mStringBlock[offset] = (byte) strBytes.length; - offset++; - for (int i = 0; i < strBytes.length; i++, offset++) { - mStringBlock[offset] = strBytes[i]; - } - mBlockHash = Arrays.hashCode(mStringBlock); + block[offset] = (byte) strBytes.length; + System.arraycopy(strBytes, 0, block, offset+1, strBytes.length); + mBlockHash = Arrays.hashCode(block); } // Possibly update the string block. If the native shared memory has a new block hash, @@ -1917,8 +1918,9 @@ public class PropertyInvalidatedCache<Query, Result> { // The fastest way to know that the shared memory string block has not changed. return; } - final int hash = nativeGetByteBlock(mPtr, mBlockHash, mStringBlock); - if (hash != Arrays.hashCode(mStringBlock)) { + byte[] block = new byte[mMaxByte]; + final int hash = nativeGetByteBlock(mPtr, mBlockHash, block); + if (hash != Arrays.hashCode(block)) { // This is a partial read: ignore it. The next time someone needs this string // the memory will be read again and should succeed. Set the local hash to // zero to ensure that the next read attempt will actually read from shared @@ -1930,7 +1932,7 @@ public class PropertyInvalidatedCache<Query, Result> { // The hash has changed. Update the strings from the byte block. mStringUpdated++; mBlockHash = hash; - updateStringMapLocked(); + updateStringMapLocked(block); } // Throw an exception if the string cannot be stored in the string block. @@ -1963,6 +1965,9 @@ public class PropertyInvalidatedCache<Query, Result> { } } + static final AtomicLong sStoreCount = new AtomicLong(); + + // Add a string to the local copy of the block and write the block to shared memory. // Return the index of the new string. If the string has already been recorded, the // shared memory is not updated but the index of the existing string is returned. @@ -1972,9 +1977,11 @@ public class PropertyInvalidatedCache<Query, Result> { if (handle == null) { throwIfImmutable(); throwIfBadString(str); - appendStringToMapLocked(str); - nativeSetByteBlock(mPtr, mBlockHash, mStringBlock); - updateStringMapLocked(); + byte[] block = new byte[mMaxByte]; + nativeGetByteBlock(mPtr, 0, block); + appendStringToMapLocked(str, block); + nativeSetByteBlock(mPtr, mBlockHash, block); + updateStringMapLocked(block); handle = mStringHandle.get(str); } return handle; @@ -2038,6 +2045,7 @@ public class PropertyInvalidatedCache<Query, Result> { * @param mPtr the pointer to the native shared memory. * @return the number of nonces supported by the shared memory. */ + @FastNative private static native int nativeGetMaxNonce(long mPtr); /** @@ -2046,6 +2054,7 @@ public class PropertyInvalidatedCache<Query, Result> { * @param mPtr the pointer to the native shared memory. * @return the number of string bytes supported by the shared memory. */ + @FastNative private static native int nativeGetMaxByte(long mPtr); /** diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java index e2a72dd5e385..7875c23be038 100644 --- a/core/java/android/os/IpcDataCache.java +++ b/core/java/android/os/IpcDataCache.java @@ -257,6 +257,7 @@ import java.util.concurrent.atomic.AtomicLong; */ @TestApi @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, Result> { /** * {@inheritDoc} diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 81dc46ecbd3a..60a9e053e99d 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -20,17 +20,22 @@ import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH; import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH; import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH; +import static com.android.window.flags.Flags.balStrictMode; import android.animation.ValueAnimator; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.app.ActivityThread; import android.app.IActivityManager; +import android.app.IBackgroundActivityLaunchCallback; import android.app.IUnsafeIntentStrictModeCallback; +import android.app.PendingIntent; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -45,6 +50,7 @@ import android.content.res.Configuration; import android.net.TrafficStats; import android.net.Uri; import android.os.storage.IStorageManager; +import android.os.strictmode.BackgroundActivityLaunchViolation; import android.os.strictmode.CleartextNetworkViolation; import android.os.strictmode.ContentUriWithoutPermissionViolation; import android.os.strictmode.CredentialProtectedWhileLockedViolation; @@ -82,6 +88,7 @@ import com.android.internal.os.RuntimeInit; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.HexDump; import com.android.internal.util.Preconditions; +import com.android.window.flags.Flags; import dalvik.system.BlockGuard; import dalvik.system.CloseGuard; @@ -266,6 +273,7 @@ public final class StrictMode { DETECT_VM_IMPLICIT_DIRECT_BOOT, DETECT_VM_INCORRECT_CONTEXT_USE, DETECT_VM_UNSAFE_INTENT_LAUNCH, + DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED, PENALTY_GATHER, PENALTY_LOG, PENALTY_DIALOG, @@ -309,6 +317,8 @@ public final class StrictMode { private static final int DETECT_VM_INCORRECT_CONTEXT_USE = 1 << 12; /** @hide */ private static final int DETECT_VM_UNSAFE_INTENT_LAUNCH = 1 << 13; + /** @hide */ + private static final int DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED = 1 << 14; /** @hide */ private static final int DETECT_VM_ALL = 0x0000ffff; @@ -902,6 +912,9 @@ public final class StrictMode { if (targetSdk >= Build.VERSION_CODES.S) { detectUnsafeIntentLaunch(); } + if (balStrictMode() && targetSdk > Build.VERSION_CODES.VANILLA_ICE_CREAM) { + detectBlockedBackgroundActivityLaunch(); + } // TODO: Decide whether to detect non SDK API usage beyond a certain API level. // TODO: enable detectImplicitDirectBoot() once system is less noisy @@ -1140,6 +1153,39 @@ public final class StrictMode { } /** + * Detects when your app is blocked from launching a background activity or a + * PendingIntent created by your app cannot be launched. + * <p> + * Starting an activity requires <a + * href="https://developer.android.com/guide/components/activities/background-starts + * ">specific permissions</a> which may depend on the state at runtime and especially + * in case of {@link android.app.PendingIntent} starts on the collaborating app. + * If the activity start is blocked methods like {@link Context#startActivity(Intent)} + * or {@link PendingIntent#send()} have no way to return that information. Instead you + * can use this strct mode feature to detect blocked starts. + * <p> + * Note that in some cases blocked starts may be unavoidable, e.g. when the user clicks + * the home button while the app tries to start a new activity. + */ + @SuppressWarnings("BuilderSetStyle") + @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE) + public @NonNull Builder detectBlockedBackgroundActivityLaunch() { + return enable(DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED); + } + + /** + * Stops detecting whether your app is blocked from launching a background activity or + * a PendingIntent created by your app cannot be launched. + * <p> + * This disables the effect of {@link #detectBlockedBackgroundActivityLaunch()}. + */ + @SuppressWarnings("BuilderSetStyle") + @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE) + public @NonNull Builder ignoreBlockedBackgroundActivityLaunch() { + return disable(DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED); + } + + /** * Crashes the whole process on violation. This penalty runs at the end of all enabled * penalties so you'll still get your logging or other violations before the process * dies. @@ -2133,10 +2179,25 @@ public final class StrictMode { registerIntentMatchingRestrictionCallback(); } + if ((sVmPolicy.mask & DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED) != 0) { + registerBackgroundActivityLaunchCallback(); + } + setBlockGuardVmPolicy(sVmPolicy.mask); } } + private static void registerBackgroundActivityLaunchCallback() { + try { + ActivityTaskManager.getService().registerBackgroundActivityStartCallback( + new BackgroundActivityLaunchCallback()); + } catch (DeadObjectException e) { + // ignore + } catch (RemoteException e) { + Log.e(TAG, "RemoteException handling StrictMode violation", e); + } + } + private static final class UnsafeIntentStrictModeCallback extends IUnsafeIntentStrictModeCallback.Stub { @Override @@ -2161,6 +2222,16 @@ public final class StrictMode { } } + private static final class BackgroundActivityLaunchCallback + extends IBackgroundActivityLaunchCallback.Stub { + @Override + public void onBackgroundActivityLaunchAborted(String message) { + if (StrictMode.vmBackgroundActivityLaunchEnabled()) { + StrictMode.onBackgroundActivityLaunchAborted(message); + } + } + } + /** Gets the current VM policy. */ public static VmPolicy getVmPolicy() { synchronized (StrictMode.class) { @@ -2236,6 +2307,11 @@ public final class StrictMode { } /** @hide */ + public static boolean vmBackgroundActivityLaunchEnabled() { + return (sVmPolicy.mask & DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED) != 0; + } + + /** @hide */ public static void onSqliteObjectLeaked(String message, Throwable originStack) { onVmPolicyViolation(new SqliteObjectLeakedViolation(message, originStack)); } @@ -2402,6 +2478,11 @@ public final class StrictMode { onVmPolicyViolation(new UnsafeIntentLaunchViolation(intent, msg + intent)); } + /** @hide */ + public static void onBackgroundActivityLaunchAborted(String message) { + onVmPolicyViolation(new BackgroundActivityLaunchViolation(message)); + } + /** Assume locked until we hear otherwise */ private static volatile boolean sCeStorageUnlocked = false; diff --git a/core/java/android/os/strictmode/BackgroundActivityLaunchViolation.java b/core/java/android/os/strictmode/BackgroundActivityLaunchViolation.java new file mode 100644 index 000000000000..aef52c6f275f --- /dev/null +++ b/core/java/android/os/strictmode/BackgroundActivityLaunchViolation.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.strictmode; + +import android.annotation.NonNull; +import android.app.Activity; + +/** + * Violation raised when your app is blocked from launching an {@link Activity} + * (from the background). + * <p> + * This occurs when the app: + * <ul> + * <li>Does not have sufficient privileges to launch the Activity.</li> + * <li>Has not explicitly opted-in to launch the Activity.</li> + * </ul> + * Violations may affect the functionality of your app and should be addressed to ensure + * proper behavior. + * @hide + */ +public class BackgroundActivityLaunchViolation extends Violation { + + /** @hide */ + public BackgroundActivityLaunchViolation(@NonNull String message) { + super(message); + } +} diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 59c66532fe0b..35d791156ea3 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1083,7 +1083,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } - boolean isPredictiveBackImeHideAnimInProgress() { + public boolean isPredictiveBackImeHideAnimInProgress() { return mIsPredictiveBackImeHideAnimInProgress; } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 2d2f4c958a73..7dc77b175c79 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -2400,10 +2400,14 @@ public final class InputMethodManager { if (Flags.refactorInsetsController()) { final var viewRootImpl = view.getViewRootImpl(); // In case of a running show IME animation, it should not be requested visible, - // otherwise the animation would jump and not be controlled by the user anymore - if (viewRootImpl != null - && (viewRootImpl.getInsetsController().computeUserAnimatingTypes() - & WindowInsets.Type.ime()) == 0) { + // otherwise the animation would jump and not be controlled by the user anymore. + // If predictive back is in progress, and a editText is focussed, we should + // show the IME. + if (viewRootImpl != null && ( + (viewRootImpl.getInsetsController().computeUserAnimatingTypes() + & WindowInsets.Type.ime()) == 0 + || viewRootImpl.getInsetsController() + .isPredictiveBackImeHideAnimInProgress())) { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_NO_ONGOING_USER_ANIMATION); if (resultReceiver != null) { diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 3f3d3d8bb2fe..731d10048d7c 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -376,3 +376,10 @@ flag { description: "Enables PiP features in desktop mode." bug: "350475854" } + +flag { + name: "enable_desktop_windowing_hsum" + namespace: "lse_desktop_experience" + description: "Enables HSUM on desktop mode." + bug: "366397912" +}
\ No newline at end of file diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index aee1c3b2f28c..54262e82004c 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -266,6 +266,7 @@ android_ravenwood_test { "src/android/content/ContextTest.java", "src/android/content/pm/PackageManagerTest.java", "src/android/content/pm/UserInfoTest.java", + "src/android/app/PropertyInvalidatedCacheTests.java", "src/android/database/CursorWindowTest.java", "src/android/os/**/*.java", "src/android/content/res/*.java", diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index 65153f55295a..e417181a4463 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -52,11 +52,7 @@ import org.junit.Test; * atest FrameworksCoreTests:PropertyInvalidatedCacheTests */ @SmallTest -@IgnoreUnderRavenwood(blockedBy = PropertyInvalidatedCache.class) public class PropertyInvalidatedCacheTests { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -389,9 +385,11 @@ public class PropertyInvalidatedCacheTests { assertEquals(n1, "cache_key.bluetooth.get_state"); } - // Verify that test mode works properly. + // Verify that invalidating the cache from an app process would fail due to lack of permissions. @Test - public void testTestMode() { + @android.platform.test.annotations.DisabledOnRavenwood( + reason = "SystemProperties doesn't have permission check") + public void testPermissionFailure() { // Create a cache that will write a system nonce. TestCache sysCache = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode1"); try { @@ -403,6 +401,13 @@ public class PropertyInvalidatedCacheTests { // The expected exception is a bare RuntimeException. The test does not attempt to // validate the text of the exception message. } + } + + // Verify that test mode works properly. + @Test + public void testTestMode() { + // Create a cache that will write a system nonce. + TestCache sysCache = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode1"); sysCache.testPropertyName(); // Invalidate the cache. This must succeed because the property has been marked for @@ -441,6 +446,8 @@ public class PropertyInvalidatedCacheTests { // storing nonces in shared memory. @RequiresFlagsEnabled(FLAG_APPLICATION_SHARED_MEMORY_ENABLED) @Test + @android.platform.test.annotations.DisabledOnRavenwood( + reason = "PIC doesn't use SharedMemory on Ravenwood") public void testSharedMemoryStorage() { // Fetch a shared memory instance for testing. ApplicationSharedMemory shmem = ApplicationSharedMemory.create(); diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java index 5c56fdcd4a2a..2fc1ffd6a453 100644 --- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java +++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java @@ -41,10 +41,7 @@ import org.junit.Test; * atest FrameworksCoreTests:IpcDataCacheTest */ @SmallTest -@IgnoreUnderRavenwood(blockedBy = IpcDataCache.class) public class IpcDataCacheTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); // Configuration for creating caches private static final String MODULE = IpcDataCache.MODULE_TEST; @@ -452,9 +449,11 @@ public class IpcDataCacheTest { assertEquals(ec.isDisabled(), true); } - // Verify that test mode works properly. + // Verify that invalidating the cache from an app process would fail due to lack of permissions. @Test - public void testTestMode() { + @android.platform.test.annotations.DisabledOnRavenwood( + reason = "SystemProperties doesn't have permission check") + public void testPermissionFailure() { // Create a cache that will write a system nonce. TestCache sysCache = new TestCache(IpcDataCache.MODULE_SYSTEM, "mode1"); try { @@ -466,6 +465,13 @@ public class IpcDataCacheTest { // The expected exception is a bare RuntimeException. The test does not attempt to // validate the text of the exception message. } + } + + // Verify that test mode works properly. + @Test + public void testTestMode() { + // Create a cache that will write a system nonce. + TestCache sysCache = new TestCache(IpcDataCache.MODULE_SYSTEM, "mode1"); sysCache.testPropertyName(); // Invalidate the cache. This must succeed because the property has been marked for diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png Binary files differindex 5b429c0eaf7c..15af624b8609 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png Binary files differindex 6028fa21a8fd..85ce24bea51a 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png Binary files differindex a163d926a2bd..a1d0e7ba9453 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png Binary files differindex 25d2e34c52b4..3bc2ae7dfe73 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml index 4a42616a45ec..4c7d1c7339fb 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml @@ -22,7 +22,6 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="@dimen/bubble_popup_margin_top" android:layout_marginHorizontal="@dimen/bubble_popup_margin_horizontal" - android:layout_marginBottom="@dimen/bubble_popup_margin_bottom" android:elevation="@dimen/bubble_popup_elevation" android:gravity="center_horizontal" android:orientation="vertical"> @@ -30,7 +29,7 @@ <ImageView android:layout_width="@dimen/bubble_popup_icon_size" android:layout_height="@dimen/bubble_popup_icon_size" - android:tint="?androidprv:attr/materialColorPrimary" + android:tint="?androidprv:attr/materialColorOutline" android:contentDescription="@null" android:src="@drawable/pip_ic_settings"/> @@ -49,6 +48,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/bubble_popup_text_margin" + android:paddingBottom="@dimen/bubble_popup_padding_bottom" android:maxWidth="@dimen/bubble_popup_content_max_width" android:textAppearance="@android:style/TextAppearance.DeviceDefault" android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml index f19c3c762d9d..345c399652f9 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml @@ -22,7 +22,6 @@ android:layout_gravity="bottom|end" android:layout_marginTop="@dimen/bubble_popup_margin_top" android:layout_marginHorizontal="@dimen/bubble_popup_margin_horizontal" - android:layout_marginBottom="@dimen/bubble_popup_margin_bottom" android:elevation="@dimen/bubble_popup_elevation" android:gravity="center_horizontal" android:orientation="vertical"> @@ -49,6 +48,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/bubble_popup_text_margin" + android:paddingBottom="@dimen/bubble_popup_padding_bottom" android:maxWidth="@dimen/bubble_popup_content_max_width" android:textAppearance="@android:style/TextAppearance.DeviceDefault" android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 6cd380d1eed9..fa1aa193e1e3 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -239,11 +239,11 @@ <!-- Max width for the bubble popup view. --> <dimen name="bubble_popup_content_max_width">300dp</dimen> <!-- Horizontal margin for the bubble popup view. --> - <dimen name="bubble_popup_margin_horizontal">32dp</dimen> + <dimen name="bubble_popup_margin_horizontal">24dp</dimen> <!-- Top margin for the bubble bar education views. --> <dimen name="bubble_popup_margin_top">24dp</dimen> - <!-- Bottom margin for the bubble bar education views. --> - <dimen name="bubble_popup_margin_bottom">32dp</dimen> + <!-- Bottom padding for the bubble bar education views. --> + <dimen name="bubble_popup_padding_bottom">8dp</dimen> <!-- Text margin for the bubble bar education views. --> <dimen name="bubble_popup_text_margin">16dp</dimen> <!-- Size of icons in the bubble bar education views. --> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 510c9b742e74..4531b7932eaf 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -489,7 +489,6 @@ <activity android:name=".touchpad.tutorial.ui.view.TouchpadTutorialActivity" android:exported="true" android:showForAllUsers="true" - android:screenOrientation="userLandscape" android:theme="@style/Theme.AppCompat.NoActionBar"> <intent-filter> <action android:name="com.android.systemui.action.TOUCHPAD_TUTORIAL"/> @@ -500,7 +499,6 @@ <activity android:name=".inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity" android:exported="true" android:showForAllUsers="true" - android:screenOrientation="userLandscape" android:theme="@style/Theme.AppCompat.NoActionBar"> <intent-filter> <action android:name="com.android.systemui.action.TOUCHPAD_KEYBOARD_TUTORIAL"/> diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index d4f3b5b6d6a6..28a12f813cf5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -78,9 +78,7 @@ fun ColumnVolumeSliders( ) { require(viewModels.isNotEmpty()) Column(modifier = modifier) { - Box( - modifier = Modifier.fillMaxWidth(), - ) { + Box(modifier = Modifier.fillMaxWidth()) { val sliderViewModel: SliderViewModel = viewModels.first() val sliderState by viewModels.first().slider.collectAsStateWithLifecycle() val sliderPadding by topSliderPadding(isExpandable) @@ -94,6 +92,7 @@ fun ColumnVolumeSliders( onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, + hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory, ) ExpandButton( @@ -143,6 +142,7 @@ fun ColumnVolumeSliders( onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, + hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory, ) } } @@ -181,7 +181,7 @@ private fun ExpandButton( colors = IconButtonDefaults.filledIconButtonColors( containerColor = sliderColors.indicatorColor, - contentColor = sliderColors.iconColor + contentColor = sliderColors.iconColor, ), ) { Icon( @@ -211,9 +211,7 @@ private fun enterTransition(index: Int, totalCount: Int): EnterTransition { animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay), clip = false, ) + - fadeIn( - animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay), - ) + fadeIn(animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay)) } private fun exitTransition(index: Int, totalCount: Int): ExitTransition { @@ -286,6 +284,6 @@ private fun topSliderPadding(isExpandable: Boolean): State<Dp> { 0.dp }, animationSpec = animationSpec, - label = "TopVolumeSliderPadding" + label = "TopVolumeSliderPadding", ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt index d15430faa0a0..a0e46d51c73a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt @@ -49,6 +49,7 @@ fun GridVolumeSliders( onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, + hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index a23bb67215b5..eb79b906e5f8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -22,6 +22,8 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size @@ -46,9 +48,14 @@ import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.dp import com.android.compose.PlatformSlider import com.android.compose.PlatformSliderColors +import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig +import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState @Composable @@ -59,8 +66,40 @@ fun VolumeSlider( onIconTapped: () -> Unit, modifier: Modifier = Modifier, sliderColors: PlatformSliderColors, + hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) { val value by valueState(state) + val interactionSource = remember { MutableInteractionSource() } + val sliderStepSize = 1f / (state.valueRange.endInclusive - state.valueRange.start) + val hapticsViewModel: SliderHapticsViewModel? = + if (Flags.hapticsForComposeSliders()) { + rememberViewModel(traceName = "SliderHapticsViewModel") { + hapticsViewModelFactory.create( + interactionSource, + state.valueRange, + Orientation.Horizontal, + SliderHapticFeedbackConfig( + lowerBookendScale = 0.2f, + progressBasedDragMinScale = 0.2f, + progressBasedDragMaxScale = 0.5f, + deltaProgressForDragThreshold = 0f, + additionalVelocityMaxBump = 0.2f, + maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */ + sliderStepSize = sliderStepSize, + ), + SeekableSliderTrackerConfig( + lowerBookendThreshold = 0f, + upperBookendThreshold = 1f, + ), + ) + } + } else { + null + } + + // Perform haptics due to UI composition + hapticsViewModel?.onValueChange(value) + PlatformSlider( modifier = modifier.sysuiResTag(state.label).clearAndSetSemantics { @@ -94,7 +133,7 @@ fun VolumeSlider( val newValue = (value + targetDirection * state.a11yStep).coerceIn( state.valueRange.start, - state.valueRange.endInclusive + state.valueRange.endInclusive, ) onValueChange(newValue) true @@ -102,16 +141,18 @@ fun VolumeSlider( }, value = value, valueRange = state.valueRange, - onValueChange = onValueChange, - onValueChangeFinished = onValueChangeFinished, + onValueChange = { newValue -> + hapticsViewModel?.addVelocityDataPoint(newValue) + onValueChange(newValue) + }, + onValueChangeFinished = { + hapticsViewModel?.onValueChangeEnded() + onValueChangeFinished?.invoke() + }, enabled = state.isEnabled, icon = { state.icon?.let { - SliderIcon( - icon = it, - onIconTapped = onIconTapped, - isTappable = state.isMutable, - ) + SliderIcon(icon = it, onIconTapped = onIconTapped, isTappable = state.isMutable) } }, colors = sliderColors, @@ -128,7 +169,8 @@ fun VolumeSlider( disabledMessage = state.disabledMessage, ) } - } + }, + interactionSource = interactionSource, ) } @@ -150,14 +192,14 @@ private fun SliderIcon( icon: Icon, onIconTapped: () -> Unit, isTappable: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val boxModifier = if (isTappable) { modifier.clickable( onClick = onIconTapped, interactionSource = null, - indication = null + indication = null, ) } else { modifier diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt index 3467382df4da..75fd56658d9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt @@ -47,7 +47,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val config = SliderHapticFeedbackConfig() + private var config = SliderHapticFeedbackConfig() private val dragVelocityProvider = SliderDragVelocityProvider { config.maxVelocityToScale } @@ -227,6 +227,72 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playHapticAtProgress_forDiscreteSlider_playsTick() = + with(kosmos) { + config = SliderHapticFeedbackConfig(sliderStepSize = 0.2f) + sliderHapticFeedbackProvider = + SliderHapticFeedbackProvider( + vibratorHelper, + msdlPlayer, + dragVelocityProvider, + config, + kosmos.fakeSystemClock, + ) + + // GIVEN max velocity and slider progress + val progress = 1f + val expectedScale = + sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress) + val tick = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, expectedScale) + .compose() + + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) + + // WHEN called to play haptics + sliderHapticFeedbackProvider.onProgress(progress) + + // THEN the correct composition only plays once + assertEquals(expected = 1, vibratorHelper.timesVibratedWithEffect(tick)) + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playHapticAtProgress_forDiscreteSlider_playsDiscreteSliderToken() = + with(kosmos) { + config = SliderHapticFeedbackConfig(sliderStepSize = 0.2f) + sliderHapticFeedbackProvider = + SliderHapticFeedbackProvider( + vibratorHelper, + msdlPlayer, + dragVelocityProvider, + config, + kosmos.fakeSystemClock, + ) + + // GIVEN max velocity and slider progress + val progress = 1f + val expectedScale = + sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress) + val expectedProperties = + InteractionProperties.DynamicVibrationScale(expectedScale, pipeliningAttributes) + + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) + + // WHEN called to play haptics + sliderHapticFeedbackProvider.onProgress(progress) + + // THEN the correct token plays once + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(expectedProperties) + assertThat(msdlPlayer.getHistory().size).isEqualTo(1) + } + + @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playHapticAtProgress_onQuickSuccession_playsContinuousDragTokenOnce() = with(kosmos) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt index d96e6643962d..2e2894d43b12 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.composefragment.viewmodel import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope @@ -33,7 +34,9 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() { protected val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/AnnouncementResolverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/AnnouncementResolverTest.kt index 2e8498a6da28..6580e3b17072 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/AnnouncementResolverTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/AnnouncementResolverTest.kt @@ -16,7 +16,7 @@ package com.android.systemui.screenshot -import android.testing.AndroidTestingRunner +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.systemui.kosmos.Kosmos import com.android.systemui.screenshot.data.repository.profileTypeRepository import com.android.systemui.screenshot.policy.TestUserIds @@ -30,7 +30,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock -@RunWith(AndroidTestingRunner::class) +@RunWith(AndroidJUnit4::class) class AnnouncementResolverTest { private val kosmos = Kosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureFrameworkSmokeTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureFrameworkSmokeTest.java index 5699cfc96c8a..a5d239a0f8b9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureFrameworkSmokeTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureFrameworkSmokeTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.fail; import android.content.Intent; import android.os.RemoteException; -import android.testing.AndroidTestingRunner; import android.util.Log; import android.view.Display; import android.view.IScrollCaptureResponseListener; @@ -30,6 +29,7 @@ import android.view.IWindowManager; import android.view.ScrollCaptureResponse; import android.view.WindowManagerGlobal; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -45,7 +45,7 @@ import java.util.concurrent.TimeUnit; /** * Tests the of internal framework Scroll Capture API from SystemUI. */ -@RunWith(AndroidTestingRunner.class) +@RunWith(AndroidJUnit4.class) @SmallTest @Ignore public class ScrollCaptureFrameworkSmokeTest extends SysuiTestCase { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt index 119abd144b11..637a12c11690 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt @@ -16,9 +16,9 @@ package com.android.systemui.settings.brightness -import android.testing.AndroidTestingRunner import android.view.MotionEvent import android.widget.SeekBar +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.settingslib.RestrictedLockUtils @@ -52,7 +52,7 @@ import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(AndroidJUnit4::class) class BrightnessSliderControllerTest : SysuiTestCase() { @Mock private lateinit var brightnessSliderView: BrightnessSliderView diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt index 8f41caf54ec8..e38ea30daf0e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.call.domain.interactor +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -27,8 +28,10 @@ import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith @SmallTest +@RunWith(AndroidJUnit4::class) class CallChipInteractorTest : SysuiTestCase() { val kosmos = Kosmos() val repo = kosmos.ongoingCallRepository diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 7bc6d4ae2816..3ebf9f72b5ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -20,6 +20,7 @@ import android.app.PendingIntent import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase @@ -40,11 +41,13 @@ import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class CallChipViewModelTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt index ecb1a6d44b22..dd0ac1cc4ce5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.domian.interactor +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -30,8 +31,10 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith @SmallTest +@RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class MediaRouterChipInteractorTest : SysuiTestCase() { val kosmos = Kosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt index e33ce9ccaaa6..b297bed61ecb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith import android.content.ComponentName import android.content.DialogInterface import android.content.Intent @@ -49,6 +51,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt index e6101f500ad1..9e8f22e331ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view import android.content.DialogInterface import android.content.applicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -35,12 +36,14 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify @SmallTest +@RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index 01e55011a175..c511c433d92d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.DialogInterface import android.platform.test.annotations.EnableFlags import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP @@ -53,6 +54,7 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor @@ -62,6 +64,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt index c62e40414121..795988f0087f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.content.packageManager import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos @@ -31,6 +32,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -38,6 +40,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class EndMediaProjectionDialogHelperTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt new file mode 100644 index 000000000000..19ed6a57d2f0 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt @@ -0,0 +1,70 @@ +/* + * 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.chips.notification.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(StatusBarNotifChips.FLAG_NAME) +class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope + + private val underTest = kosmos.statusBarNotificationChipsInteractor + + @Test + fun onPromotedNotificationChipTapped_emitsKeys() = + testScope.runTest { + val latest by collectValues(underTest.promotedNotificationChipTapEvent) + + underTest.onPromotedNotificationChipTapped("fakeKey") + + assertThat(latest).hasSize(1) + assertThat(latest[0]).isEqualTo("fakeKey") + + underTest.onPromotedNotificationChipTapped("fakeKey2") + + assertThat(latest).hasSize(2) + assertThat(latest[1]).isEqualTo("fakeKey2") + } + + @Test + fun onPromotedNotificationChipTapped_sameKeyTwice_emitsTwice() = + testScope.runTest { + val latest by collectValues(underTest.promotedNotificationChipTapEvent) + + underTest.onPromotedNotificationChipTapped("fakeKey") + underTest.onPromotedNotificationChipTapped("fakeKey") + + assertThat(latest).hasSize(2) + assertThat(latest[0]).isEqualTo("fakeKey") + assertThat(latest[1]).isEqualTo("fakeKey") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index eb5d9318c88f..6e190965d08b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -17,12 +17,14 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import android.platform.test.annotations.EnableFlags +import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.notification.data.model.activeNotificationModel @@ -102,6 +104,30 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest!![1], secondIcon) } + @Test + fun chips_clickingChipNotifiesInteractor() = + testScope.runTest { + val latest by collectLastValue(underTest.chips) + val latestChipTap by + collectLastValue( + kosmos.statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent + ) + + setNotifs( + listOf( + activeNotificationModel( + key = "clickTest", + statusBarChipIcon = mock<StatusBarIconView>(), + ) + ) + ) + val chip = latest!![0] + + chip.onClickListener!!.onClick(mock<View>()) + + assertThat(latestChipTap).isEqualTo("clickTest") + } + private fun setNotifs(notifs: List<ActiveNotificationModel>) { activeNotificationListRepository.activeNotifications.value = ActiveNotificationsStore.Builder() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt index 6bfb40fa17c5..0efd591940f2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.screenrecord.domain.interactor +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -32,8 +33,10 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith @SmallTest +@RunWith(AndroidJUnit4::class) class ScreenRecordChipInteractorTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt index bfb63ac66d3d..709e0b57c02a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt @@ -23,6 +23,7 @@ import android.content.Intent import android.content.applicationContext import android.content.packageManager import android.content.pm.ApplicationInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos @@ -39,6 +40,7 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq @@ -47,6 +49,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class EndScreenRecordingDialogDelegateTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 16101bfe387c..bfebe184ae2d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.content.DialogInterface import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase @@ -48,6 +49,7 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor @@ -57,6 +59,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class ScreenRecordChipViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index d7d57c87f48c..b3dec2eaa1c6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.DialogInterface import android.platform.test.annotations.EnableFlags import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP @@ -51,6 +52,7 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor @@ -60,6 +62,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class ShareToAppChipViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt index 4977c548fb92..8d4c68de8c79 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import androidx.annotation.DrawableRes +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon @@ -34,8 +35,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith @SmallTest +@RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class ChipTransitionHelperTest : SysuiTestCase() { private val kosmos = Kosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt index 6e4d8863fee2..e3510f5ce280 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase @@ -28,6 +29,7 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener import com.android.systemui.statusbar.phone.SystemUIDialog import kotlin.test.Test +import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -35,6 +37,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class OngoingActivityChipViewModelTest : SysuiTestCase() { private val mockSystemUIDialog = mock<SystemUIDialog>() private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 76bb8de71bdd..9613f76c2b48 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -17,12 +17,18 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification.GROUP_ALERT_ALL import android.app.Notification.GROUP_ALERT_SUMMARY +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.notification.HeadsUpManagerPhone import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -32,6 +38,8 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.mockNotifCollection +import com.android.systemui.statusbar.notification.collection.notifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback @@ -43,9 +51,9 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback -import com.android.systemui.statusbar.notification.HeadsUpManagerPhone import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -54,6 +62,7 @@ import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock import java.util.ArrayList import java.util.function.Consumer +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -73,6 +82,11 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @RunWithLooper class HeadsUpCoordinatorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val statusBarNotificationChipsInteractor = kosmos.statusBarNotificationChipsInteractor + private val notifCollection = kosmos.mockNotifCollection + private lateinit var coordinator: HeadsUpCoordinator // captured listeners and pluggables: @@ -115,16 +129,19 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { helper = NotificationGroupTestHelper(mContext) coordinator = HeadsUpCoordinator( + kosmos.applicationCoroutineScope, logger, systemClock, + notifCollection, headsUpManager, headsUpViewBinder, visualInterruptionDecisionProvider, remoteInputManager, launchFullScreenIntentProvider, flags, + statusBarNotificationChipsInteractor, headerController, - executor + executor, ) coordinator.attach(notifPipeline) @@ -351,7 +368,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { assertFalse( notifLifetimeExtender.maybeExtendLifetime( NotificationEntryBuilder().setPkg("test-package").build(), - /* reason= */ 0 + /* reason= */ 0, ) ) } @@ -442,6 +459,97 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun showPromotedNotification_hasNotifEntry_shownAsHUN() = + testScope.runTest { + whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) + + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + finishBind(entry) + verify(headsUpManager).showNotification(entry) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun showPromotedNotification_noNotifEntry_noHUN() = + testScope.runTest { + whenever(notifCollection.getEntry(entry.key)).thenReturn(null) + + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any()) + verify(headsUpManager, never()).showNotification(entry) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() = + testScope.runTest { + whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) + + // First, add the entry as shouldn't HUN + setShouldHeadsUp(entry, false) + collectionListener.onEntryAdded(entry) + beforeTransformGroupsListener.onBeforeTransformGroups(listOf(entry)) + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // WHEN that entry becomes a promoted notification and is tapped + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN it's still shown as heads up + finishBind(entry) + verify(headsUpManager).showNotification(entry) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() = + testScope.runTest { + // First, the promoted notification appears as not heads up + val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build() + whenever(notifCollection.getEntry(promotedEntry.key)).thenReturn(promotedEntry) + setShouldHeadsUp(promotedEntry, false) + + collectionListener.onEntryAdded(promotedEntry) + beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry)) + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry)) + + verify(headsUpViewBinder, never()).bindHeadsUpView(eq(promotedEntry), any()) + verify(headsUpManager, never()).showNotification(promotedEntry) + + // Then a new notification comes in that should be heads up + setShouldHeadsUp(entry, false) + whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) + collectionListener.onEntryAdded(entry) + + // At the same time, the promoted notification chip is tapped + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(promotedEntry.key) + executor.advanceClockToLast() + executor.runAllReady() + + // WHEN we finalize the pipeline + beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry, entry)) + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry, entry)) + + // THEN the promoted entry is shown as a HUN, *not* the new entry + finishBind(promotedEntry) + verify(headsUpManager).showNotification(promotedEntry) + + verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any()) + verify(headsUpManager, never()).showNotification(entry) + } + + @Test fun testTransferIsolatedChildAlert_withGroupAlertSummary() { setShouldHeadsUp(groupSummary) whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1)) @@ -862,7 +970,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider).launchFullScreenIntent(entry) verifyLoggedFullScreenIntentDecision( entry, - FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE + FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) } @@ -885,7 +993,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, - FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND + FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) } @@ -899,7 +1007,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, - FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND + FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) clearInterruptionProviderInvocations() @@ -917,7 +1025,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(headsUpManager, never()).showNotification(any()) verifyLoggedFullScreenIntentDecision( entry, - FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE + FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) clearInterruptionProviderInvocations() @@ -942,7 +1050,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, - FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND + FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) clearInterruptionProviderInvocations() @@ -975,7 +1083,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(headsUpManager, never()).showNotification(any()) verifyLoggedFullScreenIntentDecision( entry, - FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE + FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) clearInterruptionProviderInvocations() } @@ -1070,7 +1178,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private fun setShouldFullScreen( entry: NotificationEntry, - originalDecision: FullScreenIntentDecision + originalDecision: FullScreenIntentDecision, ) { whenever(visualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)) .thenAnswer { FullScreenIntentDecisionImpl(entry, originalDecision) } @@ -1078,7 +1186,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private fun verifyLoggedFullScreenIntentDecision( entry: NotificationEntry, - originalDecision: FullScreenIntentDecision + originalDecision: FullScreenIntentDecision, ) { val decision = withArgCaptor { verify(visualInterruptionDecisionProvider).logFullScreenIntentDecision(capture()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt index ebec00380cf4..3d7603368ebc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt @@ -21,13 +21,13 @@ import com.android.settingslib.graph.SignalDrawable import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.google.common.truth.Truth.assertThat +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import org.junit.runners.Parameterized.Parameters @SmallTest -@RunWith(Parameterized::class) +@RunWith(ParameterizedAndroidJunit4::class) internal class SignalIconModelParameterizedTest(private val testCase: TestCase) : SysuiTestCase() { @Test fun drawableFromModel_level0_numLevels4_noExclamation_notCarrierNetworkChange() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java index e3bd88523e8e..4557182c7114 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java @@ -30,7 +30,6 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; -import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt index 16061df1fa89..1b7b47f49af2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt @@ -23,6 +23,7 @@ import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.media.MediaRouter import android.media.projection.MediaProjectionInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.logcatLogBuffer @@ -30,6 +31,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.policy.CastDevice.Companion.toCastDevice import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.Mockito.doAnswer import org.mockito.kotlin.any @@ -38,6 +40,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class CastDeviceTest : SysuiTestCase() { private val mockAppInfo = mock<ApplicationInfo>().apply { whenever(this.loadLabel(any())).thenReturn("") } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt index f80b36a10dc2..d3071f87f744 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor @@ -73,6 +74,7 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() { kosmos.zenModeInteractor, kosmos.uiEventLogger, kosmos.volumePanelLogger, + kosmos.sliderHapticsViewModelFactory, ) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 61832875dc2e..b431636b0e8b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -255,11 +255,6 @@ object Flags { val WM_ENABLE_SHELL_TRANSITIONS = sysPropBooleanFlag("persist.wm.debug.shell_transit", default = true) - // TODO(b/256873975): Tracking Bug - @JvmField - @Keep - val WM_BUBBLE_BAR = sysPropBooleanFlag("persist.wm.debug.bubble_bar", default = false) - // TODO(b/254513207): Tracking Bug to delete @Keep @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt index 24dd04dbadd2..da124de358a8 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt @@ -47,4 +47,6 @@ data class SliderHapticFeedbackConfig( @FloatRange(from = 0.0, to = 1.0) val lowerBookendScale: Float = 0.05f, /** Exponent for power function compensation */ @FloatRange(from = 0.0, fromInclusive = false) val exponent: Float = 1f / 0.89f, + /** The step-size that defines the slider quantization. Zero represents a continuous slider */ + @FloatRange(from = 0.0) val sliderStepSize: Float = 0f, ) diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt index bc4f531b8b81..de6697bd8fea 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt @@ -30,6 +30,7 @@ import com.google.android.msdl.domain.MSDLPlayer import kotlin.math.abs import kotlin.math.min import kotlin.math.pow +import kotlin.math.round /** * Listener of slider events that triggers haptic feedback. @@ -124,14 +125,45 @@ class SliderHapticFeedbackProvider( val deltaProgress = abs(normalizedSliderProgress - dragTextureLastProgress) if (deltaProgress < config.deltaProgressForDragThreshold) return + // Check if the progress is a discrete step so haptics can be delivered + if ( + config.sliderStepSize > 0 && + !normalizedSliderProgress.isDiscreteStep(config.sliderStepSize) + ) { + return + } + val powerScale = scaleOnDragTexture(absoluteVelocity, normalizedSliderProgress) // Deliver haptic feedback - performContinuousSliderDragVibration(powerScale) + when { + config.sliderStepSize == 0f -> performContinuousSliderDragVibration(powerScale) + config.sliderStepSize > 0f -> performDiscreteSliderDragVibration(powerScale) + } dragTextureLastTime = currentTime dragTextureLastProgress = normalizedSliderProgress } + private fun Float.isDiscreteStep(stepSize: Float, epsilon: Float = 0.001f): Boolean { + if (stepSize <= 0f) return false + val division = this / stepSize + return abs(division - round(division)) < epsilon + } + + private fun performDiscreteSliderDragVibration(scale: Float) { + if (Flags.msdlFeedback()) { + val properties = + InteractionProperties.DynamicVibrationScale(scale, VIBRATION_ATTRIBUTES_PIPELINING) + msdlPlayer.playToken(MSDLToken.DRAG_INDICATOR_DISCRETE, properties) + } else { + val effect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, scale) + .compose() + vibratorHelper.vibrate(effect, VIBRATION_ATTRIBUTES_PIPELINING) + } + } + private fun performContinuousSliderDragVibration(scale: Float) { if (Flags.msdlFeedback()) { val properties = diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderQuantization.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderQuantization.kt new file mode 100644 index 000000000000..033d55cc9b61 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderQuantization.kt @@ -0,0 +1,26 @@ +/* + * 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.haptics.slider + +interface SliderQuantization { + /** What is the step size between discrete steps of the slider */ + val stepSize: Float + + data class Continuous(override val stepSize: Float = Float.MIN_VALUE) : SliderQuantization + + data class Discrete(override val stepSize: Float) : SliderQuantization +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt index de242597f463..7fa83c64d5eb 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt @@ -143,18 +143,20 @@ constructor( SliderEventType.STARTED_TRACKING_TOUCH -> { startingProgress = normalized currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_USER + sliderStateProducer.onProgressChanged(true, normalized) } SliderEventType.PROGRESS_CHANGE_BY_USER -> { - velocityTracker.addPosition(System.currentTimeMillis(), normalized.toOffset()) + addVelocityDataPoint(value) currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_USER sliderStateProducer.onProgressChanged(true, normalized) } SliderEventType.STARTED_TRACKING_PROGRAM -> { startingProgress = normalized currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_PROGRAM + sliderStateProducer.onProgressChanged(false, normalized) } SliderEventType.PROGRESS_CHANGE_BY_PROGRAM -> { - velocityTracker.addPosition(System.currentTimeMillis(), normalized.toOffset()) + addVelocityDataPoint(value) currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_PROGRAM sliderStateProducer.onProgressChanged(false, normalized) } @@ -162,6 +164,11 @@ constructor( } } + fun addVelocityDataPoint(value: Float) { + val normalized = value.normalize() + velocityTracker.addPosition(System.currentTimeMillis(), normalized.toOffset()) + } + fun onValueChangeEnded() { when (currentSliderEventType) { SliderEventType.STARTED_TRACKING_PROGRAM, @@ -174,8 +181,10 @@ constructor( velocityTracker.resetTracking() } + private fun ClosedFloatingPointRange<Float>.length(): Float = endInclusive - start + private fun Float.normalize(): Float = - (this / (sliderRange.endInclusive - sliderRange.start)).coerceIn(0f, 1f) + ((this - sliderRange.start) / sliderRange.length()).coerceIn(0f, 1f) private fun Float.toOffset(): Offset = when (orientation) { diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt index 3d2baee9b936..edc7c1d6361f 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt @@ -16,8 +16,8 @@ package com.android.systemui.inputdevice.tutorial.ui.composable +import android.content.res.Configuration import androidx.annotation.RawRes -import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.foundation.background @@ -36,6 +36,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished @@ -66,23 +67,13 @@ fun ActionTutorialContent( .safeDrawingPadding() .padding(start = 48.dp, top = 100.dp, end = 48.dp, bottom = 8.dp), ) { - Row(modifier = Modifier.fillMaxWidth().weight(1f)) { - TutorialDescription( - titleTextId = - if (actionState is Finished) config.strings.titleSuccessResId - else config.strings.titleResId, - titleColor = config.colors.title, - bodyTextId = - if (actionState is Finished) config.strings.bodySuccessResId - else config.strings.bodyResId, - modifier = Modifier.weight(1f), - ) - Spacer(modifier = Modifier.width(76.dp)) - TutorialAnimation( - actionState, - config, - modifier = Modifier.weight(1f).padding(top = 8.dp), - ) + when (LocalConfiguration.current.orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + HorizontalDescriptionAndAnimation(actionState, config, Modifier.weight(1f)) + } + else -> { + VerticalDescriptionAndAnimation(actionState, config, Modifier.weight(1f)) + } } AnimatedVisibility(visible = actionState is Finished, enter = fadeIn()) { DoneButton(onDoneButtonClicked = onDoneButtonClicked) @@ -91,17 +82,56 @@ fun ActionTutorialContent( } @Composable +private fun HorizontalDescriptionAndAnimation( + actionState: TutorialActionState, + config: TutorialScreenConfig, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier.fillMaxWidth()) { + TutorialDescription(actionState, config, modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(70.dp)) + TutorialAnimation(actionState, config, modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun VerticalDescriptionAndAnimation( + actionState: TutorialActionState, + config: TutorialScreenConfig, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxWidth().padding(horizontal = 40.dp, vertical = 40.dp)) { + Spacer(modifier = Modifier.weight(0.1f)) + TutorialDescription( + actionState, + config, + modifier = + Modifier.weight(0.2f) + // extra padding to better align with animation which has embedded padding + .padding(horizontal = 15.dp), + ) + Spacer(modifier = Modifier.width(70.dp)) + TutorialAnimation(actionState, config, modifier = Modifier.weight(1f)) + } +} + +@Composable fun TutorialDescription( - @StringRes titleTextId: Int, - titleColor: Color, - @StringRes bodyTextId: Int, + actionState: TutorialActionState, + config: TutorialScreenConfig, modifier: Modifier = Modifier, ) { + val (titleTextId, bodyTextId) = + if (actionState is Finished) { + config.strings.titleSuccessResId to config.strings.bodySuccessResId + } else { + config.strings.titleResId to config.strings.bodyResId + } Column(verticalArrangement = Arrangement.Top, modifier = modifier) { Text( text = stringResource(id = titleTextId), style = MaterialTheme.typography.displayLarge, - color = titleColor, + color = config.colors.title, ) Spacer(modifier = Modifier.height(16.dp)) Text( diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt index 720c01fc7056..2be619bac998 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt @@ -24,11 +24,13 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.node.Ref import androidx.compose.ui.util.lerp @@ -49,7 +51,7 @@ fun TutorialAnimation( config: TutorialScreenConfig, modifier: Modifier = Modifier, ) { - Box(modifier = modifier.fillMaxWidth()) { + Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxWidth()) { AnimatedContent( targetState = actionState::class, transitionSpec = { @@ -97,6 +99,7 @@ private fun EducationAnimation( composition = composition, progress = { progress }, dynamicProperties = animationProperties, + modifier = Modifier.fillMaxSize(), ) } @@ -112,6 +115,7 @@ private fun SuccessAnimation( composition = composition, progress = { progress }, dynamicProperties = animationProperties, + modifier = Modifier.fillMaxSize(), ) } @@ -142,6 +146,7 @@ private fun InProgressAnimation( composition = composition, progress = { lerp(start = startProgress, stop = endProgress, fraction = progress) }, dynamicProperties = animationProperties, + modifier = Modifier.fillMaxSize(), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt new file mode 100644 index 000000000000..9e09671bc7bf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt @@ -0,0 +1,48 @@ +/* + * 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.chips.notification.domain.interactor + +import android.annotation.SuppressLint +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +/** An interactor for the notification chips shown in the status bar. */ +@SysUISingleton +class StatusBarNotificationChipsInteractor @Inject constructor() { + + // Each chip tap is an individual event, *not* a state, which is why we're using SharedFlow not + // StateFlow. There shouldn't be multiple updates per frame, which should avoid performance + // problems. + @SuppressLint("SharedFlowCreation") + private val _promotedNotificationChipTapEvent = MutableSharedFlow<String>() + + /** + * SharedFlow that emits each time a promoted notification's status bar chip is tapped. The + * emitted value is the promoted notification's key. + */ + val promotedNotificationChipTapEvent: SharedFlow<String> = + _promotedNotificationChipTapEvent.asSharedFlow() + + suspend fun onPromotedNotificationChipTapped(key: String) { + StatusBarNotifChips.assertInNewMode() + _promotedNotificationChipTapEvent.emit(key) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 6ae92637bde7..c8d3f339b3e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -16,20 +16,30 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel +import android.view.View import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch /** A view model for status bar chips for promoted ongoing notifications. */ @SysUISingleton class NotifChipsViewModel @Inject -constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { +constructor( + @Application private val applicationScope: CoroutineScope, + activeNotificationsInteractor: ActiveNotificationsInteractor, + private val notifChipsInteractor: StatusBarNotificationChipsInteractor, +) { /** * A flow modeling the notification chips that should be shown. Emits an empty list if there are * no notifications that should show a status bar chip. @@ -44,13 +54,20 @@ constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { * notification has invalid data such that it can't be displayed as a chip. */ private fun ActiveNotificationModel.toChipModel(): OngoingActivityChipModel.Shown? { + StatusBarNotifChips.assertInNewMode() // TODO(b/364653005): Log error if there's no icon view. val rawIcon = this.statusBarChipIconView ?: return null val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(rawIcon) // TODO(b/364653005): Use the notification color if applicable. val colors = ColorsModel.Themed - // TODO(b/364653005): When the chip is clicked, show the HUN. - val onClickListener = null + val onClickListener = + View.OnClickListener { + // The notification pipeline needs everything to run on the main thread, so keep + // this event on the main thread. + applicationScope.launch { + notifChipsInteractor.onPromotedNotificationChipTapped(this@toChipModel.key) + } + } return OngoingActivityChipModel.Shown.ShortTimeDelta( icon, colors, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index ec8566b82aea..c7b47eeec218 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -20,11 +20,15 @@ import android.app.Notification.GROUP_ALERT_SUMMARY import android.util.ArrayMap import android.util.ArraySet import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope @@ -48,6 +52,8 @@ import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Coordinates heads up notification (HUN) interactions with the notification pipeline based on the @@ -67,16 +73,19 @@ import javax.inject.Inject class HeadsUpCoordinator @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val mLogger: HeadsUpCoordinatorLogger, private val mSystemClock: SystemClock, + private val notifCollection: NotifCollection, private val mHeadsUpManager: HeadsUpManager, private val mHeadsUpViewBinder: HeadsUpViewBinder, private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider, private val mRemoteInputManager: NotificationRemoteInputManager, private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider, private val mFlags: NotifPipelineFlags, + private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor, @IncomingHeader private val mIncomingHeaderController: NodeController, - @Main private val mExecutor: DelayableExecutor + @Main private val mExecutor: DelayableExecutor, ) : Coordinator { private val mEntriesBindingUntil = ArrayMap<String, Long>() private val mEntriesUpdateTimes = ArrayMap<String, Long>() @@ -98,6 +107,52 @@ constructor( pipeline.addPromoter(mNotifPromoter) pipeline.addNotificationLifetimeExtender(mLifetimeExtender) mRemoteInputManager.addActionPressListener(mActionPressListener) + + if (StatusBarNotifChips.isEnabled) { + applicationScope.launch { + statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect { + showPromotedNotificationHeadsUp(it) + } + } + } + } + + /** + * Shows the promoted notification with the given [key] as heads-up. + * + * Must be run on the main thread. + */ + private fun showPromotedNotificationHeadsUp(key: String) { + StatusBarNotifChips.assertInNewMode() + mLogger.logShowPromotedNotificationHeadsUp(key) + + val entry = notifCollection.getEntry(key) + if (entry == null) { + mLogger.logPromotedNotificationForHeadsUpNotFound(key) + return + } + // TODO(b/364653005): Validate that the given key indeed matches a promoted notification, + // not just any notification. + + val posted = + PostedEntry( + entry, + wasAdded = false, + wasUpdated = false, + // Force-set this notification to show heads-up. + // TODO(b/364653005): This means that if you tap on the second notification chip, + // then it moves to become the first chip because whatever notification is showing + // heads-up is considered to be the top notification. + shouldHeadsUpEver = true, + shouldHeadsUpAgain = true, + isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key), + isBinding = isEntryBinding(entry), + ) + + mExecutor.execute { + mPostedEntries[entry.key] = posted + mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}") + } } private fun onHeadsUpViewBound(entry: NotificationEntry) { @@ -222,7 +277,7 @@ constructor( logicalSummary.setInterruption() mLogger.logSummaryMarkedInterrupted( logicalSummary.key, - childToReceiveParentHeadsUp.key + childToReceiveParentHeadsUp.key, ) // If the summary was not attached, then remove the heads up from the detached @@ -246,12 +301,12 @@ constructor( handlePostedEntry( summaryUpdateForRemoval, hunMutator, - scenario = "detached-summary-remove-heads-up" + scenario = "detached-summary-remove-heads-up", ) } else if (summaryUpdate != null) { mLogger.logPostedEntryWillNotEvaluate( summaryUpdate, - reason = "attached-summary-transferred" + reason = "attached-summary-transferred", ) } @@ -270,14 +325,14 @@ constructor( handlePostedEntry( postedEntry, hunMutator, - scenario = "child-heads-up-transfer-target-$targetType" + scenario = "child-heads-up-transfer-target-$targetType", ) didHeadsUpChildToReceiveParentHeadsUp = true } else { handlePostedEntry( postedEntry, hunMutator, - scenario = "child-heads-up-non-target" + scenario = "child-heads-up-non-target", ) } } @@ -301,7 +356,7 @@ constructor( handlePostedEntry( posted, hunMutator, - scenario = "non-posted-child-heads-up-transfer-target-$targetType" + scenario = "non-posted-child-heads-up-transfer-target-$targetType", ) } } @@ -345,10 +400,7 @@ constructor( .filter { !it.sbn.notification.isGroupSummary } .filter { locationLookupByKey(it.key) != GroupLocation.Detached } .sortedWith( - compareBy( - { !mPostedEntries.contains(it.key) }, - { -it.sbn.notification.getWhen() }, - ) + compareBy({ !mPostedEntries.contains(it.key) }, { -it.sbn.notification.getWhen() }) ) .firstOrNull() @@ -499,7 +551,7 @@ constructor( mHeadsUpManager.removeNotification( posted.key, /* removeImmediately= */ false, - "onEntryUpdated" + "onEntryUpdated", ) } else if (posted.isBinding) { // Don't let the bind finish @@ -527,7 +579,7 @@ constructor( mHeadsUpManager.removeNotification( entry.key, removeImmediatelyForRemoteInput, - "onEntryRemoved, reason: $reason" + "onEntryRemoved, reason: $reason", ) } } @@ -593,7 +645,7 @@ constructor( // for FSI reconsideration mLogger.logEntryDisqualifiedFromFullScreen( entry.key, - decision.logReason + decision.logReason, ) mVisualInterruptionDecisionProvider.logFullScreenIntentDecision( decision @@ -619,7 +671,7 @@ constructor( mLogger.logEntryUpdatedByRanking( entry.key, shouldHeadsUpEver, - decision.logReason + decision.logReason, ) onEntryUpdated(entry) } @@ -731,10 +783,10 @@ constructor( entry.key, /* releaseImmediately */ true, "cancel lifetime extension - extended for reason: " + - "$reason, isSticky: true" + "$reason, isSticky: true", ) }, - removeAfterMillis + removeAfterMillis, ) } else { mExecutor.execute { @@ -742,7 +794,7 @@ constructor( entry.key, /* releaseImmediately */ false, "lifetime extension - extended for reason: $reason" + - ", isSticky: false" + ", isSticky: false", ) } mNotifsExtendingLifetime[entry] = null @@ -873,7 +925,7 @@ private enum class GroupLocation { Detached, Isolated, Summary, - Child + Child, } private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index 1a521d767438..e443a0418ffd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -138,4 +138,22 @@ class HeadsUpCoordinatorLogger(private val buffer: LogBuffer, private val verbos { "marked group summary as interrupted: $str1 for alert transfer to child: $str2" }, ) } + + fun logShowPromotedNotificationHeadsUp(key: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "requesting promoted entry to show heads up: $str1" }, + ) + } + + fun logPromotedNotificationForHeadsUpNotFound(key: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "could not find promoted entry, so not showing heads up: $str1" }, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index c2093114c98f..d371acf86a28 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -16,15 +16,16 @@ package com.android.systemui.touchpad.tutorial.ui.composable +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -40,6 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp @@ -60,6 +62,7 @@ fun TutorialSelectionScreen( modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainer) .fillMaxSize() + .safeDrawingPadding() .pointerInteropFilter( onTouchEvent = { event -> // Because of window flag we're intercepting 3 and 4-finger swipes. @@ -69,12 +72,26 @@ fun TutorialSelectionScreen( } ), ) { - TutorialSelectionButtons( - onBackTutorialClicked = onBackTutorialClicked, - onHomeTutorialClicked = onHomeTutorialClicked, - onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, - modifier = Modifier.padding(60.dp), - ) + val configuration = LocalConfiguration.current + when (configuration.orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + HorizontalSelectionButtons( + onBackTutorialClicked = onBackTutorialClicked, + onHomeTutorialClicked = onHomeTutorialClicked, + onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, + modifier = Modifier.weight(1f).padding(60.dp), + ) + } + else -> { + VerticalSelectionButtons( + onBackTutorialClicked = onBackTutorialClicked, + onHomeTutorialClicked = onHomeTutorialClicked, + onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, + modifier = Modifier.weight(1f).padding(60.dp), + ) + } + } + // because other composables have weight 1, Done button will be positioned first DoneButton( onDoneButtonClicked = onDoneButtonClicked, modifier = Modifier.padding(horizontal = 60.dp), @@ -83,7 +100,7 @@ fun TutorialSelectionScreen( } @Composable -private fun TutorialSelectionButtons( +private fun HorizontalSelectionButtons( onBackTutorialClicked: () -> Unit, onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, @@ -94,34 +111,70 @@ private fun TutorialSelectionButtons( verticalAlignment = Alignment.CenterVertically, modifier = modifier, ) { - TutorialButton( - text = stringResource(R.string.touchpad_tutorial_home_gesture_button), - icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon), - iconColor = MaterialTheme.colorScheme.onPrimary, - onClick = onHomeTutorialClicked, - backgroundColor = MaterialTheme.colorScheme.primary, - modifier = Modifier.weight(1f), + ThreeTutorialButtons( + onBackTutorialClicked, + onHomeTutorialClicked, + onRecentAppsTutorialClicked, + modifier = Modifier.weight(1f).fillMaxSize(), ) - TutorialButton( - text = stringResource(R.string.touchpad_tutorial_back_gesture_button), - icon = Icons.AutoMirrored.Outlined.ArrowBack, - iconColor = MaterialTheme.colorScheme.onTertiary, - onClick = onBackTutorialClicked, - backgroundColor = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.weight(1f), - ) - TutorialButton( - text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button), - icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_recents_icon), - iconColor = MaterialTheme.colorScheme.onSecondary, - onClick = onRecentAppsTutorialClicked, - backgroundColor = MaterialTheme.colorScheme.secondary, - modifier = Modifier.weight(1f), + } +} + +@Composable +private fun VerticalSelectionButtons( + onBackTutorialClicked: () -> Unit, + onHomeTutorialClicked: () -> Unit, + onRecentAppsTutorialClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + ThreeTutorialButtons( + onBackTutorialClicked, + onHomeTutorialClicked, + onRecentAppsTutorialClicked, + modifier = Modifier.weight(1f).fillMaxSize(), ) } } @Composable +private fun ThreeTutorialButtons( + onBackTutorialClicked: () -> Unit, + onHomeTutorialClicked: () -> Unit, + onRecentAppsTutorialClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + TutorialButton( + text = stringResource(R.string.touchpad_tutorial_home_gesture_button), + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon), + iconColor = MaterialTheme.colorScheme.onPrimary, + onClick = onHomeTutorialClicked, + backgroundColor = MaterialTheme.colorScheme.primary, + modifier = modifier, + ) + TutorialButton( + text = stringResource(R.string.touchpad_tutorial_back_gesture_button), + icon = Icons.AutoMirrored.Outlined.ArrowBack, + iconColor = MaterialTheme.colorScheme.onTertiary, + onClick = onBackTutorialClicked, + backgroundColor = MaterialTheme.colorScheme.tertiary, + modifier = modifier, + ) + TutorialButton( + text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button), + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_recents_icon), + iconColor = MaterialTheme.colorScheme.onSecondary, + onClick = onRecentAppsTutorialClicked, + backgroundColor = MaterialTheme.colorScheme.secondary, + modifier = modifier, + ) +} + +@Composable private fun TutorialButton( text: String, icon: ImageVector, @@ -134,7 +187,7 @@ private fun TutorialButton( onClick = onClick, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = backgroundColor), - modifier = modifier.aspectRatio(0.66f), + modifier = modifier, ) { Column( verticalArrangement = Arrangement.Center, diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 639b46a64fd1..bd4c4639e57a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -2698,7 +2698,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, /* velocityAxis= */ MotionEvent.AXIS_Y, /* upperBookendScale= */ 1f, /* lowerBookendScale= */ 0.05f, - /* exponent= */ 1f / 0.89f); + /* exponent= */ 1f / 0.89f, + /* sliderStepSize = */ 0f); private static final SeekableSliderTrackerConfig sSliderTrackerConfig = new SeekableSliderTrackerConfig( /* waitTimeMillis= */100, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 1f44451f0cb4..68f789600dd0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -28,6 +28,7 @@ import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.common.shared.model.Icon +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.modes.shared.ModesUiIcons import com.android.systemui.res.R import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor @@ -61,6 +62,7 @@ constructor( private val zenModeInteractor: ZenModeInteractor, private val uiEventLogger: UiEventLogger, private val volumePanelLogger: VolumePanelLogger, + override val hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) : SliderViewModel { private val volumeChanges = MutableStateFlow<Int?>(null) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index bb849cb1333e..8efe915abda2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.session.MediaController.PlaybackInfo import com.android.systemui.common.shared.model.Icon +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession @@ -40,6 +41,7 @@ constructor( @Assisted private val coroutineScope: CoroutineScope, private val context: Context, private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, + override val hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) : SliderViewModel { override val slider: StateFlow<SliderState> = diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt index 7ded8c5c9fc1..9c1783b99f78 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import kotlinx.coroutines.flow.StateFlow /** Controls the behaviour of a volume slider. */ @@ -23,6 +24,8 @@ interface SliderViewModel { val slider: StateFlow<SliderState> + val hapticsViewModelFactory: SliderHapticsViewModel.Factory + fun onValueChanged(state: SliderState, newValue: Float) fun onValueChangeFinished() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt new file mode 100644 index 000000000000..74c7611a6392 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.chips.notification.domain.interactor + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor by + Kosmos.Fixture { StatusBarNotificationChipsInteractor() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt index af24c371d62b..68b28adb4b3a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt @@ -17,7 +17,15 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor val Kosmos.notifChipsViewModel: NotifChipsViewModel by - Kosmos.Fixture { NotifChipsViewModel(activeNotificationsInteractor) } + Kosmos.Fixture { + NotifChipsViewModel( + applicationCoroutineScope, + activeNotificationsInteractor, + statusBarNotificationChipsInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt index 55f0a28d0135..a78670d7f1cc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.applicationContext import com.android.internal.logging.uiEventLogger +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.volume.domain.interactor.audioVolumeInteractor @@ -40,6 +41,7 @@ val Kosmos.audioStreamSliderViewModelFactory by zenModeInteractor, uiEventLogger, volumePanelLogger, + sliderHapticsViewModelFactory, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt index f0cb2cd904ca..abd4235143f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.applicationContext +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.mediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession @@ -27,13 +28,14 @@ val Kosmos.castVolumeSliderViewModelFactory by object : CastVolumeSliderViewModel.Factory { override fun create( session: MediaDeviceSession, - coroutineScope: CoroutineScope + coroutineScope: CoroutineScope, ): CastVolumeSliderViewModel { return CastVolumeSliderViewModel( session, coroutineScope, applicationContext, mediaDeviceSessionInteractor, + sliderHapticsViewModelFactory, ) } } diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index 9c8638930df9..a26fe66da2ab 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -359,3 +359,6 @@ com.android.server.SystemService com.android.server.SystemServiceManager com.android.server.utils.TimingsTraceAndSlog + +android.os.IpcDataCache +android.app.PropertyInvalidatedCache diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 69102f197262..7cfe8292d52f 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -176,6 +176,7 @@ public class SettingsToPropertiesMapper { "core_libraries", "crumpet", "dck_framework", + "desktop_hwsec", "desktop_stats", "devoptions_settings", "game", @@ -234,6 +235,7 @@ public class SettingsToPropertiesMapper { "text", "threadnetwork", "treble", + "tv_os_media", "tv_system_ui", "usb", "vibrator", diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index b036cfc9da2b..0745fecdcd76 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5896,6 +5896,29 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } /** + * Registers an app that uses the Strict Mode for detecting BAL. + * + * @param callback the callback to register + * @return {@code true} if the callback was registered successfully. + */ + @Override + public boolean registerBackgroundActivityStartCallback(IBinder callback) { + return mTaskSupervisor.getBackgroundActivityLaunchController() + .addStrictModeCallback(Binder.getCallingUid(), callback); + } + + /** + * Unregisters an app that uses the Strict Mode for detecting BAL. + * + * @param callback the callback to unregister + */ + @Override + public void unregisterBackgroundActivityStartCallback(IBinder callback) { + mTaskSupervisor.getBackgroundActivityLaunchController() + .removeStrictModeCallback(Binder.getCallingUid(), callback); + } + + /** * Wrap the {@link ActivityOptions} in {@link SafeActivityOptions} and attach caller options * that allow using the callers permissions to start background activities. */ diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 3e553adc44f9..e55a16986285 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -52,6 +52,7 @@ import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreat import static com.android.window.flags.Flags.balRequireOptInSameUid; import static com.android.window.flags.Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid; import static com.android.window.flags.Flags.balShowToastsBlocked; +import static com.android.window.flags.Flags.balStrictMode; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -63,6 +64,7 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.AppOpsManager; import android.app.BackgroundStartPrivileges; +import android.app.IBackgroundActivityLaunchCallback; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -71,13 +73,17 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.os.IBinder; import android.os.Process; +import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.provider.DeviceConfig; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.Slog; +import android.util.SparseArray; import android.widget.Toast; import com.android.internal.R; @@ -92,6 +98,7 @@ import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfigura import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; import java.util.StringJoiner; import java.util.function.Consumer; import java.util.function.Function; @@ -143,6 +150,10 @@ public class BackgroundActivityStartController { private final ActivityTaskManagerService mService; private final ActivityTaskSupervisor mSupervisor; + @GuardedBy("mStrictModeBalCallbacks") + private final SparseArray<ArrayMap<IBinder, IBackgroundActivityLaunchCallback>> + mStrictModeBalCallbacks = new SparseArray<>(); + // TODO(b/263368846) Rename when ASM logic is moved in @Retention(SOURCE) @@ -841,7 +852,120 @@ public class BackgroundActivityStartController { // only show a toast if either caller or real caller could launch if they opted in showToast("BAL blocked. goo.gle/android-bal"); } - return statsLog(BalVerdict.BLOCK, state); + BalVerdict verdict = statsLog(BalVerdict.BLOCK, state); + if (balStrictMode()) { + String abortDebugMessage; + if (state.isPendingIntent()) { + abortDebugMessage = + "PendingIntent Activity start blocked in " + state.mRealCallingPackage + + ". " + + "PendingIntent was created in " + state.mCallingPackage + + ". " + + (state.mResultForRealCaller.allows() + ? state.mRealCallingPackage + + " could opt in to grant BAL privileges when sending. " + : "") + + (state.mResultForCaller.allows() + ? state.mCallingPackage + + " could opt in to grant BAL privileges when creating." + : "") + + "The intent would have started " + state.mIntent.getComponent(); + } else { + abortDebugMessage = "Activity start blocked. " + + "The intent would have started " + state.mIntent.getComponent(); + } + strictModeLaunchAborted(state.mCallingUid, abortDebugMessage); + if (!state.callerIsRealCaller()) { + strictModeLaunchAborted(state.mRealCallingUid, abortDebugMessage); + } + } + return verdict; + } + + /** + * Retrieve a registered strict mode callback for BAL. + * @param uid the uid of the app. + * @return the callback if it exists, returns <code>null</code> otherwise. + */ + @Nullable + Map<IBinder, IBackgroundActivityLaunchCallback> getStrictModeBalCallbacks(int uid) { + ArrayMap<IBinder, IBackgroundActivityLaunchCallback> callbackMap; + synchronized (mStrictModeBalCallbacks) { + callbackMap = + mStrictModeBalCallbacks.get(uid); + if (callbackMap == null) { + return null; + } + return new ArrayMap<>(callbackMap); + } + } + + /** + * Add strict mode callback for BAL. + * + * @param uid the UID for which the binder is registered. + * @param callback the {@link IBackgroundActivityLaunchCallback} binder to call when BAL is + * blocked. + * @return {@code true} if the callback has been successfully added. + */ + boolean addStrictModeCallback(int uid, IBinder callback) { + IBackgroundActivityLaunchCallback balCallback = + IBackgroundActivityLaunchCallback.Stub.asInterface(callback); + synchronized (mStrictModeBalCallbacks) { + ArrayMap<IBinder, IBackgroundActivityLaunchCallback> callbackMap = + mStrictModeBalCallbacks.get(uid); + if (callbackMap == null) { + callbackMap = new ArrayMap<>(); + mStrictModeBalCallbacks.put(uid, callbackMap); + } + if (callbackMap.containsKey(callback)) { + return false; + } + callbackMap.put(callback, balCallback); + } + try { + callback.linkToDeath(() -> removeStrictModeCallback(uid, callback), 0); + } catch (RemoteException e) { + removeStrictModeCallback(uid, callback); + } + return true; + } + + /** + * Remove strict mode callback for BAL. + * + * @param uid the UID for which the binder is registered. + * @param callback the {@link IBackgroundActivityLaunchCallback} binder to call when BAL is + * blocked. + */ + void removeStrictModeCallback(int uid, IBinder callback) { + synchronized (mStrictModeBalCallbacks) { + Map<IBinder, IBackgroundActivityLaunchCallback> callbackMap = + mStrictModeBalCallbacks.get(uid); + if (callback == null || !callbackMap.containsKey(callback)) { + return; + } + callbackMap.remove(callback); + if (callbackMap.isEmpty()) { + mStrictModeBalCallbacks.remove(uid); + } + } + } + + private void strictModeLaunchAborted(int callingUid, String message) { + Map<IBinder, IBackgroundActivityLaunchCallback> strictModeBalCallbacks = + getStrictModeBalCallbacks(callingUid); + if (strictModeBalCallbacks == null) { + return; + } + for (Map.Entry<IBinder, IBackgroundActivityLaunchCallback> callbackEntry : + strictModeBalCallbacks.entrySet()) { + try { + callbackEntry.getValue().onBackgroundActivityLaunchAborted(message); + } catch (RemoteException e) { + removeStrictModeCallback(callingUid, callbackEntry.getKey()); + } + } } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index 7bc9f301bcd6..db3ce0bc98eb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -194,6 +194,7 @@ public class BackgroundActivityStartControllerTests { mService.mTaskSupervisor = mSupervisor; mService.mContext = mContext; setViaReflection(mService, "mActiveUids", mActiveUids); + setViaReflection(mService, "mGlobalLock", new WindowManagerGlobalLock()); Mockito.when(mService.getPackageManagerInternalLocked()).thenReturn( mPackageManagerInternal); mService.mRootWindowContainer = mRootWindowContainer; |