summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt2
-rw-r--r--core/java/android/app/IActivityTaskManager.aidl17
-rw-r--r--core/java/android/app/IBackgroundActivityLaunchCallback.aidl26
-rw-r--r--core/java/android/app/PropertyInvalidatedCache.java73
-rw-r--r--core/java/android/os/IpcDataCache.java1
-rw-r--r--core/java/android/os/StrictMode.java81
-rw-r--r--core/java/android/os/strictmode/BackgroundActivityLaunchViolation.java41
-rw-r--r--core/java/android/view/InsetsController.java2
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java12
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig7
-rw-r--r--core/tests/coretests/Android.bp1
-rw-r--r--core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java19
-rw-r--r--core/tests/coretests/src/android/os/IpcDataCacheTest.java16
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.pngbin57190 -> 57419 bytes
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.pngbin57414 -> 57670 bytes
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.pngbin56742 -> 56970 bytes
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.pngbin56900 -> 57139 bytes
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml4
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml2
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml6
-rw-r--r--packages/SystemUI/AndroidManifest.xml2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt14
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt1
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt64
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt68
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/AnnouncementResolverTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureFrameworkSmokeTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt70
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt26
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt130
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SliderQuantization.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt74
-rw-r--r--packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt90
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt115
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt4
-rw-r--r--ravenwood/texts/ravenwood-annotation-allowed-classes.txt3
-rw-r--r--services/core/java/com/android/server/am/SettingsToPropertiesMapper.java2
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java23
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java126
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java1
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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;