summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/companion/virtual/flags.aconfig15
-rw-r--r--core/java/android/nfc/INfcCardEmulation.aidl1
-rw-r--r--core/java/android/nfc/cardemulation/ApduServiceInfo.java115
-rw-r--r--core/java/android/nfc/cardemulation/CardEmulation.java33
-rw-r--r--core/java/android/os/MessageQueue.java3
-rw-r--r--core/jni/android_os_MessageQueue.cpp2
-rw-r--r--core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java26
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java10
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java8
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java11
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java11
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java10
-rw-r--r--packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java7
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt12
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java11
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java29
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.java109
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.kt86
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java30
-rw-r--r--packages/SystemUI/aconfig/accessibility.aconfig13
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt75
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt5
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt191
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt17
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt17
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt109
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt70
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt28
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt112
-rw-r--r--packages/SystemUI/res/layout/connected_display_chip.xml1
-rw-r--r--packages/SystemUI/res/layout/connected_display_dialog.xml4
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java31
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java48
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt30
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java26
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt103
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt27
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt33
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt132
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt457
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java19
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt97
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt111
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java12
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java3
-rw-r--r--services/core/Android.bp4
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java3
-rw-r--r--services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java159
-rw-r--r--services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java5
-rw-r--r--services/core/java/com/android/server/pm/DeletePackageHelper.java5
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java10
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java13
-rw-r--r--services/core/java/com/android/server/wm/SnapshotController.java11
-rw-r--r--services/core/java/com/android/server/wm/TaskDisplayArea.java3
-rw-r--r--services/core/java/com/android/server/wm/Transition.java26
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java14
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransitionTests.java3
-rw-r--r--telephony/java/android/telephony/SubscriptionManager.java22
-rw-r--r--telephony/java/com/android/internal/telephony/ISub.aidl3
-rw-r--r--tools/aapt2/Android.bp2
-rw-r--r--tools/aapt2/cmd/Compile.cpp10
-rw-r--r--tools/aapt2/cmd/Compile.h5
-rw-r--r--tools/aapt2/cmd/Link.cpp3
-rw-r--r--tools/aapt2/link/Linkers.h28
-rw-r--r--tools/aapt2/process/ProductFilter.cpp (renamed from tools/aapt2/link/ProductFilter.cpp)70
-rw-r--r--tools/aapt2/process/ProductFilter.h65
-rw-r--r--tools/aapt2/process/ProductFilter_test.cpp (renamed from tools/aapt2/link/ProductFilter_test.cpp)95
99 files changed, 2413 insertions, 778 deletions
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 55ae8eec35d9..f380963fbcab 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -37,10 +37,17 @@ flag {
}
flag {
- name: "virtual_camera"
- namespace: "virtual_devices"
- description: "Enable Virtual Camera"
- bug: "270352264"
+ name: "virtual_camera"
+ namespace: "virtual_devices"
+ description: "Enable Virtual Camera"
+ bug: "270352264"
+}
+
+flag {
+ name: "stream_camera"
+ namespace: "virtual_devices"
+ description: "Enable streaming camera to Virtual Devices"
+ bug: "291740640"
}
flag {
diff --git a/core/java/android/nfc/INfcCardEmulation.aidl b/core/java/android/nfc/INfcCardEmulation.aidl
index 53843fe73d33..c7b3b2c03f65 100644
--- a/core/java/android/nfc/INfcCardEmulation.aidl
+++ b/core/java/android/nfc/INfcCardEmulation.aidl
@@ -40,5 +40,6 @@ interface INfcCardEmulation
boolean unsetPreferredService();
boolean supportsAidPrefixRegistration();
ApduServiceInfo getPreferredPaymentService(int userHandle);
+ boolean setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status);
boolean isDefaultPaymentRegistered();
}
diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
index 665b7531d3ce..9cf8c4ddc53b 100644
--- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -127,6 +127,11 @@ public final class ApduServiceInfo implements Parcelable {
private final String mSettingsActivityName;
/**
+ * State of the service for CATEGORY_OTHER selection
+ */
+ private boolean mOtherServiceSelectionState;
+
+ /**
* @hide
*/
public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
@@ -134,8 +139,21 @@ public final class ApduServiceInfo implements Parcelable {
boolean requiresUnlock, int bannerResource, int uid,
String settingsActivityName, String offHost, String staticOffHost) {
this(info, onHost, description, staticAidGroups, dynamicAidGroups,
+ requiresUnlock, bannerResource, uid, settingsActivityName,
+ offHost, staticOffHost, false);
+ }
+
+ /**
+ * @hide
+ */
+ public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
+ List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
+ boolean requiresUnlock, int bannerResource, int uid,
+ String settingsActivityName, String offHost, String staticOffHost,
+ boolean isSelected) {
+ this(info, onHost, description, staticAidGroups, dynamicAidGroups,
requiresUnlock, onHost ? true : false, bannerResource, uid,
- settingsActivityName, offHost, staticOffHost);
+ settingsActivityName, offHost, staticOffHost, isSelected);
}
/**
@@ -144,7 +162,7 @@ public final class ApduServiceInfo implements Parcelable {
public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid,
- String settingsActivityName, String offHost, String staticOffHost) {
+ String settingsActivityName, String offHost, String staticOffHost, boolean isSelected) {
this.mService = info;
this.mDescription = description;
this.mStaticAidGroups = new HashMap<String, AidGroup>();
@@ -163,6 +181,8 @@ public final class ApduServiceInfo implements Parcelable {
this.mBannerResourceId = bannerResource;
this.mUid = uid;
this.mSettingsActivityName = settingsActivityName;
+ this.mOtherServiceSelectionState = isSelected;
+
}
/**
@@ -351,6 +371,9 @@ public final class ApduServiceInfo implements Parcelable {
}
// Set uid
mUid = si.applicationInfo.uid;
+
+ mOtherServiceSelectionState = false; // support other category
+
}
/**
@@ -720,43 +743,47 @@ public final class ApduServiceInfo implements Parcelable {
dest.writeInt(mBannerResourceId);
dest.writeInt(mUid);
dest.writeString(mSettingsActivityName);
+
+ dest.writeInt(mOtherServiceSelectionState ? 1 : 0);
};
@FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
public static final @NonNull Parcelable.Creator<ApduServiceInfo> CREATOR =
new Parcelable.Creator<ApduServiceInfo>() {
- @Override
- public ApduServiceInfo createFromParcel(Parcel source) {
- ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source);
- String description = source.readString();
- boolean onHost = source.readInt() != 0;
- String offHostName = source.readString();
- String staticOffHostName = source.readString();
- ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>();
- int numStaticGroups = source.readInt();
- if (numStaticGroups > 0) {
- source.readTypedList(staticAidGroups, AidGroup.CREATOR);
- }
- ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>();
- int numDynamicGroups = source.readInt();
- if (numDynamicGroups > 0) {
- source.readTypedList(dynamicAidGroups, AidGroup.CREATOR);
- }
- boolean requiresUnlock = source.readInt() != 0;
- boolean requiresScreenOn = source.readInt() != 0;
- int bannerResource = source.readInt();
- int uid = source.readInt();
- String settingsActivityName = source.readString();
- return new ApduServiceInfo(info, onHost, description, staticAidGroups,
- dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid,
- settingsActivityName, offHostName, staticOffHostName);
- }
+ @Override
+ public ApduServiceInfo createFromParcel(Parcel source) {
+ ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source);
+ String description = source.readString();
+ boolean onHost = source.readInt() != 0;
+ String offHostName = source.readString();
+ String staticOffHostName = source.readString();
+ ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>();
+ int numStaticGroups = source.readInt();
+ if (numStaticGroups > 0) {
+ source.readTypedList(staticAidGroups, AidGroup.CREATOR);
+ }
+ ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>();
+ int numDynamicGroups = source.readInt();
+ if (numDynamicGroups > 0) {
+ source.readTypedList(dynamicAidGroups, AidGroup.CREATOR);
+ }
+ boolean requiresUnlock = source.readInt() != 0;
+ boolean requiresScreenOn = source.readInt() != 0;
+ int bannerResource = source.readInt();
+ int uid = source.readInt();
+ String settingsActivityName = source.readString();
+ boolean isSelected = source.readInt() != 0;
+ return new ApduServiceInfo(info, onHost, description, staticAidGroups,
+ dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid,
+ settingsActivityName, offHostName, staticOffHostName,
+ isSelected);
+ }
- @Override
- public ApduServiceInfo[] newArray(int size) {
- return new ApduServiceInfo[size];
- }
- };
+ @Override
+ public ApduServiceInfo[] newArray(int size) {
+ return new ApduServiceInfo[size];
+ }
+ };
/**
* Dump contents for debugging.
@@ -779,14 +806,16 @@ public final class ApduServiceInfo implements Parcelable {
}
pw.println(" Static AID groups:");
for (AidGroup group : mStaticAidGroups.values()) {
- pw.println(" Category: " + group.getCategory());
+ pw.println(" Category: " + group.getCategory()
+ + "(selected: " + mOtherServiceSelectionState + ")");
for (String aid : group.getAids()) {
pw.println(" AID: " + aid);
}
}
pw.println(" Dynamic AID groups:");
for (AidGroup group : mDynamicAidGroups.values()) {
- pw.println(" Category: " + group.getCategory());
+ pw.println(" Category: " + group.getCategory()
+ + "(selected: " + mOtherServiceSelectionState + ")");
for (String aid : group.getAids()) {
pw.println(" AID: " + aid);
}
@@ -796,6 +825,22 @@ public final class ApduServiceInfo implements Parcelable {
pw.println(" Requires Device ScreenOn: " + mRequiresDeviceScreenOn);
}
+
+ /**
+ * @hide
+ */
+ public void setOtherServiceState(boolean selected) {
+ mOtherServiceSelectionState = selected;
+ }
+
+
+ /**
+ * @hide
+ */
+ public boolean isSelectedOtherService() {
+ return mOtherServiceSelectionState;
+ }
+
/**
* Dump debugging info as ApduServiceInfoProto.
*
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index d3b3a78a92cc..d048b595ad1e 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -947,6 +947,39 @@ public final class CardEmulation {
return true;
}
+ /**
+ * Allows to set or unset preferred service (category other) to avoid AID Collision.
+ *
+ * @param service The ComponentName of the service
+ * @param status true to enable, false to disable
+ * @return set service for the category and true if service is already set return false.
+ *
+ * @hide
+ */
+ public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status) {
+ if (service == null) {
+ throw new NullPointerException("activity or service or category is null");
+ }
+ int userId = mContext.getUser().getIdentifier();
+
+ try {
+ return sService.setServiceEnabledForCategoryOther(userId, service, status);
+ } catch (RemoteException e) {
+ // Try one more time
+ recoverService();
+ if (sService == null) {
+ Log.e(TAG, "Failed to recover CardEmulationService.");
+ return false;
+ }
+ try {
+ return sService.setServiceEnabledForCategoryOther(userId, service, status);
+ } catch (RemoteException ee) {
+ Log.e(TAG, "Failed to reach CardEmulationService.");
+ return false;
+ }
+ }
+ }
+
void recoverService() {
NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext);
sService = adapter.getCardEmulationService();
diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java
index 87c4f331e93f..9d8a71bf4abd 100644
--- a/core/java/android/os/MessageQueue.java
+++ b/core/java/android/os/MessageQueue.java
@@ -25,6 +25,8 @@ import android.util.Printer;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
+import dalvik.annotation.optimization.CriticalNative;
+
import java.io.FileDescriptor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -70,6 +72,7 @@ public final class MessageQueue {
private native static void nativeDestroy(long ptr);
@UnsupportedAppUsage
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+ @CriticalNative
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
diff --git a/core/jni/android_os_MessageQueue.cpp b/core/jni/android_os_MessageQueue.cpp
index 30d9ea19be39..9525605a6a8c 100644
--- a/core/jni/android_os_MessageQueue.cpp
+++ b/core/jni/android_os_MessageQueue.cpp
@@ -225,7 +225,7 @@ static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
-static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
+static void android_os_MessageQueue_nativeWake(jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
index 14f268a91abf..7ad6e8d51940 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -16,7 +16,9 @@
package com.android.server.broadcastradio.aidl;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import android.app.compat.CompatChanges;
import android.hardware.broadcastradio.AmFmBandRange;
@@ -43,14 +45,16 @@ import com.google.common.truth.Expect;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.stubbing.Answer;
import java.util.Map;
import java.util.Set;
public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase {
- private static final int U_APP_UID = 1001;
- private static final int T_APP_UID = 1002;
+ private static final int T_APP_UID = 1001;
+ private static final int U_APP_UID = 1002;
+ private static final int V_APP_UID = 1003;
private static final int FM_LOWER_LIMIT = 87_500;
private static final int FM_UPPER_LIMIT = 108_000;
@@ -133,10 +137,18 @@ public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase {
@Before
public void setUp() {
- doReturn(true).when(() -> CompatChanges.isChangeEnabled(
- ConversionUtils.RADIO_U_VERSION_REQUIRED, U_APP_UID));
- doReturn(false).when(() -> CompatChanges.isChangeEnabled(
- ConversionUtils.RADIO_U_VERSION_REQUIRED, T_APP_UID));
+ doAnswer((Answer<Boolean>) invocationOnMock -> {
+ long changeId = invocationOnMock.getArgument(0);
+ int uid = invocationOnMock.getArgument(1);
+ if (uid == V_APP_UID) {
+ return changeId == ConversionUtils.RADIO_V_VERSION_REQUIRED
+ || changeId == ConversionUtils.RADIO_U_VERSION_REQUIRED;
+ } else if (uid == U_APP_UID) {
+ return changeId == ConversionUtils.RADIO_U_VERSION_REQUIRED;
+ }
+ return false;
+ }
+ ).when(() -> CompatChanges.isChangeEnabled(anyLong(), anyInt()));
}
@Test
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 7743ad55debb..76f0b6769855 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -1280,9 +1280,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Check whether the Intent should be embedded in the known Task.
final TaskContainer taskContainer = mTaskContainers.valueAt(0);
if (taskContainer.isInPictureInPicture()
- || taskContainer.getTopNonFinishingActivity() == null) {
+ || taskContainer.getTopNonFinishingActivity(false /* includeOverlay */) == null) {
// We don't embed activity when it is in PIP, or if we can't find any other owner
- // activity in the Task.
+ // activity in non-overlay container in the Task.
return null;
}
@@ -1431,7 +1431,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
} else {
final TaskContainer taskContainer = getTaskContainer(taskId);
activityInTask = taskContainer != null
- ? taskContainer.getTopNonFinishingActivity()
+ ? taskContainer.getTopNonFinishingActivity(true /* includeOverlay */)
: null;
}
if (activityInTask == null) {
@@ -1763,10 +1763,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return;
}
- if (container.isFinished()) {
- return;
- }
-
if (container.isOverlay()) {
updateOverlayContainer(wct, container);
return;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index eeb3ccf0d4cb..028e75fe010f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -235,9 +235,13 @@ class TaskContainer {
}
@Nullable
- Activity getTopNonFinishingActivity() {
+ Activity getTopNonFinishingActivity(boolean includeOverlay) {
for (int i = mContainers.size() - 1; i >= 0; i--) {
- final Activity activity = mContainers.get(i).getTopNonFinishingActivity();
+ final TaskFragmentContainer container = mContainers.get(i);
+ if (!includeOverlay && container.isOverlay()) {
+ continue;
+ }
+ final Activity activity = container.getTopNonFinishingActivity();
if (activity != null) {
return activity;
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index e74d5fb4d0be..50cfd941adb3 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -415,6 +415,17 @@ public class OverlayPresentationTest {
}
@Test
+ public void testGetTopNonFinishingActivityWithOverlay() {
+ createTestOverlayContainer(TASK_ID, "test1");
+ final Activity activity = createMockActivity();
+ final TaskFragmentContainer container = createMockTaskFragmentContainer(activity);
+ final TaskContainer task = container.getTaskContainer();
+
+ assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */)).isEqualTo(mActivity);
+ assertThat(task.getTopNonFinishingActivity(false /* includeOverlay */)).isEqualTo(activity);
+ }
+
+ @Test
public void testUpdateContainer_dontInvokeUpdateOverlayForNonOverlayContainer() {
TaskFragmentContainer taskFragmentContainer = createMockTaskFragmentContainer(mActivity);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index e3f51697c284..e56c8ab686e7 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -151,21 +151,24 @@ public class TaskContainerTest {
@Test
public void testGetTopNonFinishingActivity() {
final TaskContainer taskContainer = createTestTaskContainer();
- assertNull(taskContainer.getTopNonFinishingActivity());
+ assertNull(taskContainer.getTopNonFinishingActivity(true /* includeOverlay */));
final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class);
taskContainer.addTaskFragmentContainer(tf0);
final Activity activity0 = mock(Activity.class);
doReturn(activity0).when(tf0).getTopNonFinishingActivity();
- assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
+ assertEquals(activity0, taskContainer.getTopNonFinishingActivity(
+ true /* includeOverlay */));
final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class);
taskContainer.addTaskFragmentContainer(tf1);
- assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
+ assertEquals(activity0, taskContainer.getTopNonFinishingActivity(
+ true /* includeOverlay */));
final Activity activity1 = mock(Activity.class);
doReturn(activity1).when(tf1).getTopNonFinishingActivity();
- assertEquals(activity1, taskContainer.getTopNonFinishingActivity());
+ assertEquals(activity1, taskContainer.getTopNonFinishingActivity(
+ true /* includeOverlay */));
}
@Test
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 51c71b1fffb7..0e59e9ad744d 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -15,10 +15,11 @@ flag {
}
flag {
- name: "desktop_windowing"
+ name: "enable_desktop_windowing"
namespace: "multitasking"
description: "Enables desktop windowing"
bug: "304778354"
+ is_fixed_read_only: true
}
flag {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 77831136b0bc..dc82fc1b35dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -18,11 +18,15 @@ package com.android.wm.shell.desktopmode;
import android.os.SystemProperties;
+import com.android.wm.shell.Flags;
+
/**
* Constants for desktop mode feature
*/
public class DesktopModeStatus {
+ private static final boolean ENABLE_DESKTOP_WINDOWING = Flags.enableDesktopWindowing();
+
/**
* Flag to indicate whether desktop mode proto is available on the device
*/
@@ -54,6 +58,12 @@ public class DesktopModeStatus {
* Return {@code true} is desktop windowing proto 2 is enabled
*/
public static boolean isEnabled() {
+ // Check for aconfig flag first
+ if (ENABLE_DESKTOP_WINDOWING) {
+ return true;
+ }
+ // Fall back to sysprop flag
+ // TODO(b/304778354): remove sysprop once desktop aconfig flag supports dynamic overriding
return IS_PROTO2_ENABLED;
}
diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
index 87bfc8111a4b..ecd500e1a160 100644
--- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
+++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
@@ -21,12 +21,13 @@ import android.util.AttributeSet;
import android.view.View;
import androidx.preference.PreferenceViewHolder;
-import androidx.preference.SwitchPreference;
+import androidx.preference.SwitchPreferenceCompat;
+
import com.android.settingslib.widget.preference.app.R;
/**
* The SwitchPreference for the pages need to show apps icon.
*/
-public class AppSwitchPreference extends SwitchPreference {
+public class AppSwitchPreference extends SwitchPreferenceCompat {
public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
@@ -52,7 +53,7 @@ public class AppSwitchPreference extends SwitchPreference {
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- final View switchView = holder.findViewById(android.R.id.switch_widget);
+ final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
if (switchView != null) {
final View rootView = switchView.getRootView();
rootView.setFilterTouchesWhenObscured(true);
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index 92fd0cd07777..95e678f446f5 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -35,7 +35,7 @@ interface IPackageManagers {
fun ApplicationInfo.hasGrantPermission(permission: String): Boolean
suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String>
- fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo?
+ fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo?
}
object PackageManagers : IPackageManagers by PackageManagersImpl(PackageManagerWrapperImpl)
@@ -72,14 +72,16 @@ internal class PackageManagersImpl(
?: false
override fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
- val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+ val packageInfo =
+ getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
return packageInfo?.requestedPermissions?.let {
permission in it
} ?: false
}
override fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
- val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+ val packageInfo =
+ getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
val index = packageInfo?.requestedPermissions?.indexOf(permission) ?: return false
return index >= 0 &&
checkNotNull(packageInfo.requestedPermissionsFlags)[index]
@@ -91,8 +93,8 @@ internal class PackageManagersImpl(
iPackageManager.isPackageAvailable(it, userId)
}.toSet()
- override fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
- packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
+ override fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo? =
+ packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags, userId)
private fun Int.hasFlag(flag: Int) = (this and flag) > 0
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
index 7fbd35b8afea..0a2d9fc3372e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
@@ -18,8 +18,11 @@ package com.android.settingslib;
import android.content.Context;
import android.util.AttributeSet;
+import android.view.Gravity;
import android.view.MotionEvent;
+import android.view.View;
import android.widget.CompoundButton;
+import android.widget.LinearLayout;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
@@ -59,13 +62,17 @@ public class PrimarySwitchPreference extends RestrictedPreference {
@Override
protected int getSecondTargetResId() {
- return R.layout.preference_widget_primary_switch;
+ return androidx.preference.R.layout.preference_widget_switch_compat;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- mSwitch = (CompoundButton) holder.findViewById(R.id.switchWidget);
+ final View widgetFrame = holder.findViewById(android.R.id.widget_frame);
+ if (widgetFrame instanceof LinearLayout linearLayout) {
+ linearLayout.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
+ }
+ mSwitch = (CompoundButton) holder.findViewById(androidx.preference.R.id.switchWidget);
if (mSwitch != null) {
mSwitch.setOnClickListener(v -> {
if (mSwitch != null && !mSwitch.isEnabled()) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index c67df71c1c77..b0832e37f35a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -16,6 +16,7 @@
package com.android.settingslib.bluetooth;
+import android.annotation.CallbackExecutor;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -39,6 +40,7 @@ import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
@@ -52,8 +54,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
import java.util.stream.Stream;
/**
@@ -101,6 +107,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>();
+ private final Map<Callback, Executor> mCallbackExecutorMap = new ConcurrentHashMap<>();
+
/**
* Last time a bt profile auto-connect was attempted.
* If an ACTION_UUID intent comes in within
@@ -992,18 +1000,39 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
return new ArrayList<>(mRemovedProfiles);
}
+ /**
+ * @deprecated Use {@link #registerCallback(Executor, Callback)}.
+ */
+ @Deprecated
public void registerCallback(Callback callback) {
mCallbacks.add(callback);
}
+ /**
+ * Registers a {@link Callback} that will be invoked when the bluetooth device attribute is
+ * changed.
+ *
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link Callback}
+ */
+ public void registerCallback(
+ @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ mCallbackExecutorMap.put(callback, executor);
+ }
+
public void unregisterCallback(Callback callback) {
mCallbacks.remove(callback);
+ mCallbackExecutorMap.remove(callback);
}
void dispatchAttributesChanged() {
for (Callback callback : mCallbacks) {
callback.onDeviceAttributesChanged();
}
+ mCallbackExecutorMap.forEach((callback, executor) ->
+ executor.execute(callback::onDeviceAttributesChanged));
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.java
deleted file mode 100644
index 6b855c00ce6e..000000000000
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2022 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.settingslib.qrcode;
-
-import android.graphics.Bitmap;
-import android.graphics.Color;
-
-import com.google.zxing.BarcodeFormat;
-import com.google.zxing.EncodeHintType;
-import com.google.zxing.MultiFormatWriter;
-import com.google.zxing.WriterException;
-import com.google.zxing.common.BitMatrix;
-
-import java.nio.charset.CharsetEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-
-public final class QrCodeGenerator {
- private static final int DEFAULT_MARGIN = -1;
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size)
- throws WriterException, IllegalArgumentException {
- return encodeQrCode(contents, size, DEFAULT_MARGIN, /*invert=*/false);
- }
-
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @param margin The margin around the actual barcode
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size, int margin)
- throws WriterException, IllegalArgumentException {
- return encodeQrCode(contents, size, margin, /*invert=*/false);
- }
-
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size, boolean invert)
- throws WriterException, IllegalArgumentException {
- return encodeQrCode(contents, size, DEFAULT_MARGIN, /*invert=*/invert);
- }
-
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @param margin The margin around the actual barcode
- * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size, int margin, boolean invert)
- throws WriterException, IllegalArgumentException {
- final Map<EncodeHintType, Object> hints = new HashMap<>();
- if (!isIso88591(contents)) {
- hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
- }
- if (margin != DEFAULT_MARGIN) {
- hints.put(EncodeHintType.MARGIN, margin);
- }
-
- final BitMatrix qrBits = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE,
- size, size, hints);
- final Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565);
- int setColor = invert ? Color.WHITE : Color.BLACK;
- int unsetColor = invert ? Color.BLACK : Color.WHITE;
- for (int x = 0; x < size; x++) {
- for (int y = 0; y < size; y++) {
- bitmap.setPixel(x, y, qrBits.get(x, y) ? setColor : unsetColor);
- }
- }
- return bitmap;
- }
-
- private static boolean isIso88591(String contents) {
- CharsetEncoder encoder = StandardCharsets.ISO_8859_1.newEncoder();
- return encoder.canEncode(contents);
- }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.kt b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.kt
new file mode 100644
index 000000000000..7b67ec6d9bec
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.qrcode
+
+import android.annotation.ColorInt
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.EncodeHintType
+import com.google.zxing.MultiFormatWriter
+import com.google.zxing.WriterException
+import java.nio.charset.StandardCharsets
+import java.util.EnumMap
+
+object QrCodeGenerator {
+ /**
+ * Generates a barcode image with [contents].
+ *
+ * @param contents The contents to encode in the barcode
+ * @param size The preferred image size in pixels
+ * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
+ * @return Barcode bitmap
+ */
+ @JvmStatic
+ @Throws(WriterException::class, java.lang.IllegalArgumentException::class)
+ fun encodeQrCode(contents: String, size: Int, invert: Boolean): Bitmap =
+ encodeQrCode(contents, size, DEFAULT_MARGIN, invert)
+
+ private const val DEFAULT_MARGIN = -1
+
+ /**
+ * Generates a barcode image with [contents].
+ *
+ * @param contents The contents to encode in the barcode
+ * @param size The preferred image size in pixels
+ * @param margin The margin around the actual barcode
+ * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
+ * @return Barcode bitmap
+ */
+ @JvmOverloads
+ @JvmStatic
+ @Throws(WriterException::class, IllegalArgumentException::class)
+ fun encodeQrCode(
+ contents: String,
+ size: Int,
+ margin: Int = DEFAULT_MARGIN,
+ invert: Boolean = false,
+ ): Bitmap {
+ val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
+ if (!isIso88591(contents)) {
+ hints[EncodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8.name()
+ }
+ if (margin != DEFAULT_MARGIN) {
+ hints[EncodeHintType.MARGIN] = margin
+ }
+ val qrBits = MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, size, size, hints)
+ @ColorInt val setColor = if (invert) Color.WHITE else Color.BLACK
+ @ColorInt val unsetColor = if (invert) Color.BLACK else Color.WHITE
+ @ColorInt val pixels = IntArray(size * size)
+ for (x in 0 until size) {
+ for (y in 0 until size) {
+ pixels[x * size + y] = if (qrBits[x, y]) setColor else unsetColor
+ }
+ }
+ return Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565).apply {
+ setPixels(pixels, 0, size, 0, 0, size, size)
+ }
+ }
+
+ private fun isIso88591(contents: String): Boolean =
+ StandardCharsets.ISO_8859_1.newEncoder().canEncode(contents)
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
index debfa49af794..851a581adf9a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
@@ -57,12 +57,14 @@ public class PrimarySwitchPreferenceTest {
com.android.settingslib.widget.preference.twotarget.R.layout.preference_two_target,
null));
mWidgetView = mHolder.itemView.findViewById(android.R.id.widget_frame);
- inflater.inflate(R.layout.preference_widget_primary_switch, mWidgetView, true);
+ inflater.inflate(androidx.preference.R.layout.preference_widget_switch_compat, mWidgetView,
+ true);
}
@Test
public void setChecked_shouldUpdateButtonCheckedState() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
mPreference.setChecked(true);
@@ -74,7 +76,8 @@ public class PrimarySwitchPreferenceTest {
@Test
public void setSwitchEnabled_shouldUpdateButtonEnabledState() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
mPreference.setSwitchEnabled(true);
@@ -86,7 +89,8 @@ public class PrimarySwitchPreferenceTest {
@Test
public void setSwitchEnabled_shouldUpdateButtonEnabledState_beforeViewBound() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.setSwitchEnabled(false);
mPreference.onBindViewHolder(mHolder);
@@ -97,7 +101,8 @@ public class PrimarySwitchPreferenceTest {
public void clickWidgetView_shouldToggleButton() {
assertThat(mWidgetView).isNotNull();
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
toggle.performClick();
@@ -111,7 +116,8 @@ public class PrimarySwitchPreferenceTest {
public void clickWidgetView_shouldNotToggleButtonIfDisabled() {
assertThat(mWidgetView).isNotNull();
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
toggle.setEnabled(false);
@@ -122,7 +128,8 @@ public class PrimarySwitchPreferenceTest {
@Test
public void clickWidgetView_shouldNotifyPreferenceChanged() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
final OnPreferenceChangeListener listener = mock(OnPreferenceChangeListener.class);
mPreference.setOnPreferenceChangeListener(listener);
@@ -139,7 +146,8 @@ public class PrimarySwitchPreferenceTest {
@Test
public void setDisabledByAdmin_hasEnforcedAdmin_shouldDisableButton() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
toggle.setEnabled(true);
mPreference.onBindViewHolder(mHolder);
@@ -149,7 +157,8 @@ public class PrimarySwitchPreferenceTest {
@Test
public void setDisabledByAdmin_noEnforcedAdmin_shouldEnableButton() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
toggle.setEnabled(false);
mPreference.onBindViewHolder(mHolder);
@@ -159,7 +168,8 @@ public class PrimarySwitchPreferenceTest {
@Test
public void onBindViewHolder_toggleButtonShouldHaveContentDescription() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
final String label = "TestButton";
mPreference.setTitle(label);
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 08ecf09b2365..25ac486ebbb4 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -3,10 +3,10 @@ package: "com.android.systemui"
# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
flag {
- name: "floating_menu_overlaps_nav_bars_flag"
+ name: "floating_menu_animated_tuck"
namespace: "accessibility"
- description: "Adjusts bounds to allow the floating menu to render on top of navigation bars."
- bug: "283768342"
+ description: "Sets up animations for tucking/untucking and adjusts clipbounds."
+ bug: "24592044"
}
flag {
@@ -14,4 +14,11 @@ flag {
namespace: "accessibility"
description: "Adds an animation for when the FAB is displaced by an IME becoming visible."
bug: "281150010"
+}
+
+flag {
+ name: "floating_menu_overlaps_nav_bars_flag"
+ namespace: "accessibility"
+ description: "Adjusts bounds to allow the floating menu to render on top of navigation bars."
+ bug: "283768342"
} \ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
new file mode 100644
index 000000000000..82d4239d7eb5
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+
+interface EdgeDetector {
+ /**
+ * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given
+ * [density] and [orientation].
+ */
+ fun edge(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): Edge?
+}
+
+val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp)
+
+/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */
+class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector {
+ override fun edge(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): Edge? {
+ val axisSize: Int
+ val axisPosition: Int
+ val topOrLeft: Edge
+ val bottomOrRight: Edge
+ when (orientation) {
+ Orientation.Horizontal -> {
+ axisSize = layoutSize.width
+ axisPosition = position.x
+ topOrLeft = Edge.Left
+ bottomOrRight = Edge.Right
+ }
+ Orientation.Vertical -> {
+ axisSize = layoutSize.height
+ axisPosition = position.y
+ topOrLeft = Edge.Top
+ bottomOrRight = Edge.Bottom
+ }
+ }
+
+ val sizePx = with(density) { size.toPx() }
+ return when {
+ axisPosition <= sizePx -> topOrLeft
+ axisPosition >= axisSize - sizePx -> bottomOrRight
+ else -> null
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index d005413fcbcf..ae7d8f599b91 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,7 +2,6 @@ package com.android.compose.animation.scene
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import kotlinx.coroutines.CoroutineScope
interface GestureHandler {
val draggable: DraggableHandler
@@ -10,9 +9,9 @@ interface GestureHandler {
}
interface DraggableHandler {
- suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset)
+ fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
fun onDelta(pixels: Float)
- suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float)
+ fun onDragStopped(velocity: Float)
}
interface NestedScrollHandler {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
new file mode 100644
index 000000000000..97d3fff48b23
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
+import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
+import androidx.compose.foundation.gestures.horizontalDrag
+import androidx.compose.foundation.gestures.verticalDrag
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.input.pointer.util.addPointerInputChange
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * Make an element draggable in the given [orientation].
+ *
+ * The main difference with [multiPointerDraggable] and
+ * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number
+ * of pointers that are down when the drag is started. If you don't need this information, you
+ * should use `draggable` instead.
+ *
+ * Note that the current implementation is trivial: we wait for the touch slope on the *first* down
+ * pointer, then we count the number of distinct pointers that are down right before calling
+ * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not
+ * dragged) and a second pointer is down and dragged. This is an implementation detail that might
+ * change in the future.
+ */
+// TODO(b/291055080): Migrate to the Modifier.Node API.
+@Composable
+internal fun Modifier.multiPointerDraggable(
+ orientation: Orientation,
+ enabled: Boolean,
+ startDragImmediately: Boolean,
+ onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
+ onDragDelta: (Float) -> Unit,
+ onDragStopped: (velocity: Float) -> Unit,
+): Modifier {
+ val onDragStarted by rememberUpdatedState(onDragStarted)
+ val onDragStopped by rememberUpdatedState(onDragStopped)
+ val onDragDelta by rememberUpdatedState(onDragDelta)
+ val startDragImmediately by rememberUpdatedState(startDragImmediately)
+
+ val velocityTracker = remember { VelocityTracker() }
+ val maxFlingVelocity =
+ LocalViewConfiguration.current.maximumFlingVelocity.let { max ->
+ val maxF = max.toFloat()
+ Velocity(maxF, maxF)
+ }
+
+ return this.pointerInput(enabled, orientation, maxFlingVelocity) {
+ if (!enabled) {
+ return@pointerInput
+ }
+
+ val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
+ velocityTracker.resetTracking()
+ onDragStarted(startedPosition, pointersDown)
+ }
+
+ val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
+
+ val onDragEnd: () -> Unit = {
+ val velocity = velocityTracker.calculateVelocity(maxFlingVelocity)
+ onDragStopped(
+ when (orientation) {
+ Orientation.Horizontal -> velocity.x
+ Orientation.Vertical -> velocity.y
+ }
+ )
+ }
+
+ val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount ->
+ velocityTracker.addPointerInputChange(change)
+ onDragDelta(amount)
+ }
+
+ detectDragGestures(
+ orientation = orientation,
+ startDragImmediately = { startDragImmediately },
+ onDragStart = onDragStart,
+ onDragEnd = onDragEnd,
+ onDragCancel = onDragCancel,
+ onDrag = onDrag,
+ )
+ }
+}
+
+/**
+ * Detect drag gestures in the given [orientation].
+ *
+ * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and
+ * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for:
+ * 1) starting the gesture immediately without requiring a drag >= touch slope;
+ * 2) passing the number of pointers down to [onDragStart].
+ */
+private suspend fun PointerInputScope.detectDragGestures(
+ orientation: Orientation,
+ startDragImmediately: () -> Boolean,
+ onDragStart: (startedPosition: Offset, pointersDown: Int) -> Unit,
+ onDragEnd: () -> Unit,
+ onDragCancel: () -> Unit,
+ onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
+) {
+ awaitEachGesture {
+ val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+ var overSlop = 0f
+ val drag =
+ if (startDragImmediately()) {
+ initialDown.consume()
+ initialDown
+ } else {
+ val down = awaitFirstDown(requireUnconsumed = false)
+ val onSlopReached = { change: PointerInputChange, over: Float ->
+ change.consume()
+ overSlop = over
+ }
+
+ // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once
+ // it is public.
+ when (orientation) {
+ Orientation.Horizontal ->
+ awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
+ Orientation.Vertical ->
+ awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
+ }
+ }
+
+ if (drag != null) {
+ // Count the number of pressed pointers.
+ val pressed = mutableSetOf<PointerId>()
+ currentEvent.changes.fastForEach { change ->
+ if (change.pressed) {
+ pressed.add(change.id)
+ }
+ }
+
+ onDragStart(drag.position, pressed.size)
+ onDrag(drag, overSlop)
+
+ val successful =
+ when (orientation) {
+ Orientation.Horizontal ->
+ horizontalDrag(drag.id) {
+ onDrag(it, it.positionChange().x)
+ it.consume()
+ }
+ Orientation.Vertical ->
+ verticalDrag(drag.id) {
+ onDrag(it, it.positionChange().y)
+ it.consume()
+ }
+ }
+
+ if (successful) {
+ onDragEnd()
+ } else {
+ onDragCancel()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 9c799b282571..3fd6828fca6b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -16,7 +16,6 @@
package com.android.compose.animation.scene
-import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -101,19 +100,3 @@ private class SceneScopeImpl(
MovableElement(layoutImpl, scene, key, modifier, content)
}
}
-
-/** The destination scene when swiping up or left from [upOrLeft]. */
-internal fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
- return when (orientation) {
- Orientation.Vertical -> userActions[Swipe.Up]
- Orientation.Horizontal -> userActions[Swipe.Left]
- }
-}
-
-/** The destination scene when swiping down or right from [downOrRight]. */
-internal fun Scene.downOrRight(orientation: Orientation): SceneKey? {
- return when (orientation) {
- Orientation.Vertical -> userActions[Swipe.Down]
- Orientation.Horizontal -> userActions[Swipe.Right]
- }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 74e66d2a9949..1f38e70799c3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
@@ -37,6 +38,7 @@ import androidx.compose.ui.platform.LocalDensity
* instance by triggering back navigation or by swiping to a new scene.
* @param transitions the definition of the transitions used to animate a change of scene.
* @param state the observable state of this layout.
+ * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
* @param scenes the configuration of the different scenes of this layout.
*/
@Composable
@@ -46,6 +48,7 @@ fun SceneTransitionLayout(
transitions: SceneTransitions,
modifier: Modifier = Modifier,
state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
+ edgeDetector: EdgeDetector = DefaultEdgeDetector,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
@@ -56,15 +59,17 @@ fun SceneTransitionLayout(
transitions,
state,
density,
+ edgeDetector,
)
}
layoutImpl.onChangeScene = onChangeScene
layoutImpl.transitions = transitions
layoutImpl.density = density
+ layoutImpl.edgeDetector = edgeDetector
+
layoutImpl.setScenes(scenes)
layoutImpl.setCurrentScene(currentScene)
-
layoutImpl.Content(modifier)
}
@@ -191,9 +196,9 @@ data class Swipe(
}
}
-enum class SwipeDirection {
- Up,
- Down,
- Left,
- Right,
+enum class SwipeDirection(val orientation: Orientation) {
+ Up(Orientation.Vertical),
+ Down(Orientation.Vertical),
+ Left(Orientation.Horizontal),
+ Right(Orientation.Horizontal),
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index a40b29991877..a803a4770517 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -47,6 +47,7 @@ class SceneTransitionLayoutImpl(
transitions: SceneTransitions,
internal val state: SceneTransitionLayoutState,
density: Density,
+ edgeDetector: EdgeDetector,
) {
internal val scenes = SnapshotStateMap<SceneKey, Scene>()
internal val elements = SnapshotStateMap<ElementKey, Element>()
@@ -57,6 +58,7 @@ class SceneTransitionLayoutImpl(
internal var onChangeScene by mutableStateOf(onChangeScene)
internal var transitions by mutableStateOf(transitions)
internal var density: Density by mutableStateOf(density)
+ internal var edgeDetector by mutableStateOf(edgeDetector)
/**
* The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 2dc53ab8bf76..ee1f13347840 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -22,8 +22,6 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
@@ -37,6 +35,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
@@ -55,7 +54,7 @@ internal fun Modifier.swipeToScene(
/** Whether swipe should be enabled in the given [orientation]. */
fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
- upOrLeft(orientation) != null || downOrRight(orientation) != null
+ userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
val currentScene = gestureHandler.currentScene
val canSwipe = currentScene.shouldEnableSwipes(orientation)
@@ -68,8 +67,7 @@ internal fun Modifier.swipeToScene(
)
return nestedScroll(connection = gestureHandler.nestedScroll.connection)
- .draggable(
- state = rememberDraggableState(onDelta = gestureHandler.draggable::onDelta),
+ .multiPointerDraggable(
orientation = orientation,
enabled = gestureHandler.isDrivingTransition || canSwipe,
// Immediately start the drag if this our [transition] is currently animating to a scene
@@ -80,6 +78,7 @@ internal fun Modifier.swipeToScene(
gestureHandler.isAnimatingOffset &&
!canOppositeSwipe,
onDragStarted = gestureHandler.draggable::onDragStarted,
+ onDragDelta = gestureHandler.draggable::onDelta,
onDragStopped = gestureHandler.draggable::onDragStopped,
)
}
@@ -159,7 +158,7 @@ class SceneGestureHandler(
internal var gestureWithPriority: Any? = null
- internal fun onDragStarted() {
+ internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
if (isDrivingTransition) {
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
@@ -199,6 +198,48 @@ class SceneGestureHandler(
Orientation.Vertical -> layoutImpl.size.height
}.toFloat()
+ val fromEdge =
+ startedPosition?.let { position ->
+ layoutImpl.edgeDetector.edge(
+ layoutImpl.size,
+ position.round(),
+ layoutImpl.density,
+ orientation,
+ )
+ }
+
+ swipeTransition.actionUpOrLeft =
+ Swipe(
+ direction =
+ when (orientation) {
+ Orientation.Horizontal -> SwipeDirection.Left
+ Orientation.Vertical -> SwipeDirection.Up
+ },
+ pointerCount = pointersDown,
+ fromEdge = fromEdge,
+ )
+
+ swipeTransition.actionDownOrRight =
+ Swipe(
+ direction =
+ when (orientation) {
+ Orientation.Horizontal -> SwipeDirection.Right
+ Orientation.Vertical -> SwipeDirection.Down
+ },
+ pointerCount = pointersDown,
+ fromEdge = fromEdge,
+ )
+
+ if (fromEdge == null) {
+ swipeTransition.actionUpOrLeftNoEdge = null
+ swipeTransition.actionDownOrRightNoEdge = null
+ } else {
+ swipeTransition.actionUpOrLeftNoEdge =
+ (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null)
+ swipeTransition.actionDownOrRightNoEdge =
+ (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null)
+ }
+
if (swipeTransition.absoluteDistance > 0f) {
transitionState = swipeTransition
}
@@ -246,11 +287,11 @@ class SceneGestureHandler(
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
val absoluteDistance = swipeTransition.absoluteDistance
- if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
+ if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) {
swipeTransition.dragOffset += absoluteDistance
swipeTransition._fromScene = toScene
} else if (
- offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key
+ offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key
) {
swipeTransition.dragOffset -= absoluteDistance
swipeTransition._fromScene = toScene
@@ -272,8 +313,8 @@ class SceneGestureHandler(
Orientation.Vertical -> layoutImpl.size.height
}.toFloat()
- val upOrLeft = upOrLeft(orientation)
- val downOrRight = downOrRight(orientation)
+ val upOrLeft = swipeTransition.upOrLeft(this)
+ val downOrRight = swipeTransition.downOrRight(this)
// Compute the target scene depending on the current offset.
return when {
@@ -516,6 +557,22 @@ class SceneGestureHandler(
var _distance by mutableFloatStateOf(0f)
val distance: Float
get() = _distance
+
+ /** The [UserAction]s associated to this swipe. */
+ var actionUpOrLeft: UserAction = Back
+ var actionDownOrRight: UserAction = Back
+ var actionUpOrLeftNoEdge: UserAction? = null
+ var actionDownOrRightNoEdge: UserAction? = null
+
+ fun upOrLeft(scene: Scene): SceneKey? {
+ return scene.userActions[actionUpOrLeft]
+ ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] }
+ }
+
+ fun downOrRight(scene: Scene): SceneKey? {
+ return scene.userActions[actionDownOrRight]
+ ?: actionDownOrRightNoEdge?.let { scene.userActions[it] }
+ }
}
companion object {
@@ -526,9 +583,9 @@ class SceneGestureHandler(
private class SceneDraggableHandler(
private val gestureHandler: SceneGestureHandler,
) : DraggableHandler {
- override suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) {
+ override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
gestureHandler.gestureWithPriority = this
- gestureHandler.onDragStarted()
+ gestureHandler.onDragStarted(pointersDown, startedPosition)
}
override fun onDelta(pixels: Float) {
@@ -537,7 +594,7 @@ private class SceneDraggableHandler(
}
}
- override suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) {
+ override fun onDragStopped(velocity: Float) {
if (gestureHandler.gestureWithPriority == this) {
gestureHandler.gestureWithPriority = null
gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
@@ -580,11 +637,31 @@ class SceneNestedScrollHandler(
// moving on to the next scene.
var gestureStartedOnNestedChild = false
+ val actionUpOrLeft =
+ Swipe(
+ direction =
+ when (gestureHandler.orientation) {
+ Orientation.Horizontal -> SwipeDirection.Left
+ Orientation.Vertical -> SwipeDirection.Up
+ },
+ pointerCount = 1,
+ )
+
+ val actionDownOrRight =
+ Swipe(
+ direction =
+ when (gestureHandler.orientation) {
+ Orientation.Horizontal -> SwipeDirection.Right
+ Orientation.Vertical -> SwipeDirection.Down
+ },
+ pointerCount = 1,
+ )
+
fun findNextScene(amount: Float): SceneKey? {
val fromScene = gestureHandler.currentScene
return when {
- amount < 0f -> fromScene.upOrLeft(gestureHandler.orientation)
- amount > 0f -> fromScene.downOrRight(gestureHandler.orientation)
+ amount < 0f -> fromScene.userActions[actionUpOrLeft]
+ amount > 0f -> fromScene.userActions[actionDownOrRight]
else -> null
}
}
@@ -625,7 +702,7 @@ class SceneNestedScrollHandler(
onStart = {
gestureHandler.gestureWithPriority = this
priorityScene = nextScene
- gestureHandler.onDragStarted()
+ gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
},
onScroll = { offsetAvailable ->
if (gestureHandler.gestureWithPriority != this) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
new file mode 100644
index 000000000000..a68282ae78f4
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FixedSizeEdgeDetectorTest {
+ private val detector = FixedSizeEdgeDetector(30.dp)
+ private val layoutSize = IntSize(100, 100)
+ private val density = Density(1f)
+
+ @Test
+ fun horizontalEdges() {
+ fun horizontalEdge(position: Int): Edge? =
+ detector.edge(
+ layoutSize,
+ position = IntOffset(position, 0),
+ density,
+ Orientation.Horizontal,
+ )
+
+ assertThat(horizontalEdge(0)).isEqualTo(Edge.Left)
+ assertThat(horizontalEdge(30)).isEqualTo(Edge.Left)
+ assertThat(horizontalEdge(31)).isEqualTo(null)
+ assertThat(horizontalEdge(69)).isEqualTo(null)
+ assertThat(horizontalEdge(70)).isEqualTo(Edge.Right)
+ assertThat(horizontalEdge(100)).isEqualTo(Edge.Right)
+ }
+
+ @Test
+ fun verticalEdges() {
+ fun verticalEdge(position: Int): Edge? =
+ detector.edge(
+ layoutSize,
+ position = IntOffset(0, position),
+ density,
+ Orientation.Vertical,
+ )
+
+ assertThat(verticalEdge(0)).isEqualTo(Edge.Top)
+ assertThat(verticalEdge(30)).isEqualTo(Edge.Top)
+ assertThat(verticalEdge(31)).isEqualTo(null)
+ assertThat(verticalEdge(69)).isEqualTo(null)
+ assertThat(verticalEdge(70)).isEqualTo(Edge.Bottom)
+ assertThat(verticalEdge(100)).isEqualTo(Edge.Bottom)
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 6791a85ff21c..1eb3392f1374 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -55,7 +55,8 @@ class SceneGestureHandlerTest {
builder = scenesBuilder,
transitions = EmptyTestTransitions,
state = layoutState,
- density = Density(1f)
+ density = Density(1f),
+ edgeDetector = DefaultEdgeDetector,
)
.also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
orientation = Orientation.Vertical,
@@ -104,13 +105,13 @@ class SceneGestureHandlerTest {
@Test
fun onDragStarted_shouldStartATransition() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
}
@Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -123,14 +124,13 @@ class SceneGestureHandlerTest {
@Test
fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
- coroutineScope = coroutineScope,
velocity = velocityThreshold - 0.01f,
)
assertScene(currentScene = SceneA, isIdle = false)
@@ -142,14 +142,13 @@ class SceneGestureHandlerTest {
@Test
fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
- coroutineScope = coroutineScope,
velocity = velocityThreshold,
)
assertScene(currentScene = SceneC, isIdle = false)
@@ -161,23 +160,22 @@ class SceneGestureHandlerTest {
@Test
fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
- draggable.onDragStopped(coroutineScope = coroutineScope, velocity = 0f)
+ draggable.onDragStopped(velocity = 0f)
assertScene(currentScene = SceneA, isIdle = true)
}
@Test
fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
- coroutineScope = coroutineScope,
velocity = velocityThreshold,
)
@@ -191,7 +189,7 @@ class SceneGestureHandlerTest {
assertScene(currentScene = SceneC, isIdle = false)
// Start a new gesture while the offset is animating
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
}
@@ -320,7 +318,7 @@ class SceneGestureHandlerTest {
}
@Test
fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
- draggable.onDragStopped(coroutineScope, velocityThreshold)
+ draggable.onDragStopped(velocityThreshold)
assertScene(currentScene = SceneA, isIdle = true)
}
@@ -332,7 +330,7 @@ class SceneGestureHandlerTest {
@Test
fun startNestedScrollWhileDragging() = runGestureTest {
- draggable.onDragStarted(coroutineScope, Offset.Zero)
+ draggable.onDragStarted(Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -344,7 +342,7 @@ class SceneGestureHandlerTest {
assertThat(transition.progress).isEqualTo(0.2f)
// this should be ignored, we are scrolling now!
- draggable.onDragStopped(coroutineScope, velocityThreshold)
+ draggable.onDragStopped(velocityThreshold)
assertScene(currentScene = SceneA, isIdle = false)
nestedScrollEvents(available = offsetY10)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index df3b72aa5533..4a6066f5c664 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -48,6 +48,14 @@ class SwipeToSceneTest {
/** The middle of the layout, in pixels. */
private val Density.middle: Offset
get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx())
+
+ /** The middle-top of the layout, in pixels. */
+ private val Density.middleTop: Offset
+ get() = Offset((LayoutWidth / 2).toPx(), 0f)
+
+ /** The middle-left of the layout, in pixels. */
+ private val Density.middleLeft: Offset
+ get() = Offset(0f, (LayoutHeight / 2).toPx())
}
private var currentScene by mutableStateOf(TestScenes.SceneA)
@@ -83,7 +91,13 @@ class SwipeToSceneTest {
}
scene(
TestScenes.SceneC,
- userActions = mapOf(Swipe.Down to TestScenes.SceneA),
+ userActions =
+ mapOf(
+ Swipe.Down to TestScenes.SceneA,
+ Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
+ Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TestScenes.SceneB,
+ Swipe(SwipeDirection.Down, fromEdge = Edge.Top) to TestScenes.SceneB,
+ ),
) {
Box(Modifier.fillMaxSize())
}
@@ -242,4 +256,100 @@ class SwipeToSceneTest {
assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
}
+
+ @Test
+ fun multiPointerSwipe() {
+ // Start at scene C.
+ currentScene = TestScenes.SceneC
+
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
+ // Swipe down with two fingers.
+ rule.onRoot().performTouchInput {
+ repeat(2) { i -> down(pointerId = i, middle) }
+ repeat(2) { i ->
+ moveBy(pointerId = i, Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
+ }
+ }
+
+ // We are transitioning to B because we used 2 fingers.
+ val transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneC)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+
+ // Release the fingers and wait for the animation to end. We are back to C because we only
+ // swiped 10dp.
+ rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
+
+ @Test
+ fun defaultEdgeSwipe() {
+ // Start at scene C.
+ currentScene = TestScenes.SceneC
+
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
+ // Swipe down from the top edge.
+ rule.onRoot().performTouchInput {
+ down(middleTop)
+ moveBy(Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
+ }
+
+ // We are transitioning to B (and not A) because we started from the top edge.
+ var transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneC)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+
+ // Release the fingers and wait for the animation to end. We are back to C because we only
+ // swiped 10dp.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
+ // Swipe right from the left edge.
+ rule.onRoot().performTouchInput {
+ down(middleLeft)
+ moveBy(Offset(touchSlop + 10.dp.toPx(), 0f), delayMillis = 1_000)
+ }
+
+ // We are transitioning to B (and not A) because we started from the left edge.
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneC)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+
+ // Release the fingers and wait for the animation to end. We are back to C because we only
+ // swiped 10dp.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
}
diff --git a/packages/SystemUI/res/layout/connected_display_chip.xml b/packages/SystemUI/res/layout/connected_display_chip.xml
index d9df91ee0a96..f9a183d3a5f7 100644
--- a/packages/SystemUI/res/layout/connected_display_chip.xml
+++ b/packages/SystemUI/res/layout/connected_display_chip.xml
@@ -41,6 +41,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="10dp"
+ android:layout_marginVertical="3dp"
android:scaleType="centerInside"
android:src="@drawable/stat_sys_connected_display"
android:tint="@android:color/black" />
diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml
index 8cfcb689eced..3f65aa7984b5 100644
--- a/packages/SystemUI/res/layout/connected_display_dialog.xml
+++ b/packages/SystemUI/res/layout/connected_display_dialog.xml
@@ -30,11 +30,11 @@
android:layout_width="@dimen/connected_display_dialog_logo_size"
android:layout_height="@dimen/connected_display_dialog_logo_size"
android:background="@drawable/circular_background"
- android:backgroundTint="?androidprv:attr/materialColorPrimary"
+ android:backgroundTint="?androidprv:attr/materialColorSecondary"
android:importantForAccessibility="no"
android:padding="6dp"
android:src="@drawable/stat_sys_connected_display"
- android:tint="?androidprv:attr/materialColorOnPrimary" />
+ android:tint="?androidprv:attr/materialColorOnSecondary" />
<TextView
android:id="@+id/connected_display_dialog_title"
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index cd8bef1ab6ed..ceddee819b4f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -341,7 +341,16 @@ class MenuAnimationController {
void moveToEdgeAndHide() {
mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ true);
final PointF position = mMenuView.getMenuPosition();
- moveToPosition(getTuckedMenuPosition());
+ final PointF tuckedPosition = getTuckedMenuPosition();
+ if (Flags.floatingMenuAnimatedTuck()) {
+ flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
+ Math.signum(tuckedPosition.x - position.x) * ESCAPE_VELOCITY,
+ FLING_FRICTION_SCALAR,
+ createDefaultSpringForce(),
+ tuckedPosition.x);
+ } else {
+ moveToPosition(tuckedPosition);
+ }
// Keep the touch region let users could click extra space to pop up the menu view
// from the screen edge
@@ -353,7 +362,24 @@ class MenuAnimationController {
void moveOutEdgeAndShow() {
mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
- mMenuView.onPositionChanged();
+ if (Flags.floatingMenuAnimatedTuck()) {
+ PointF position = mMenuView.getMenuPosition();
+ springMenuWith(DynamicAnimation.TRANSLATION_X,
+ createDefaultSpringForce(),
+ 0,
+ position.x,
+ true
+ );
+ springMenuWith(DynamicAnimation.TRANSLATION_Y,
+ createDefaultSpringForce(),
+ 0,
+ position.y,
+ true
+ );
+ } else {
+ mMenuView.onPositionChanged();
+ }
+
mMenuView.onEdgeChangedIfNeeded();
}
@@ -489,6 +515,12 @@ class MenuAnimationController {
return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
}
+ private static SpringForce createDefaultSpringForce() {
+ return new SpringForce()
+ .setStiffness(SPRING_STIFFNESS)
+ .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO);
+ }
+
static class MenuPositionProperty
extends FloatPropertyCompat<MenuView> {
private final DynamicAnimation.ViewProperty mProperty;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index ea5a56c6a0f5..92c72598cf94 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -71,6 +71,7 @@ class MenuView extends FrameLayout implements
private final MenuAnimationController mMenuAnimationController;
private OnTargetFeaturesChangeListener mFeaturesChangeListener;
+ private OnMoveToTuckedListener mMoveToTuckedListener;
MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) {
super(context);
@@ -138,6 +139,10 @@ class MenuView extends FrameLayout implements
mFeaturesChangeListener = listener;
}
+ void setMoveToTuckedListener(OnMoveToTuckedListener listener) {
+ mMoveToTuckedListener = listener;
+ }
+
void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) {
mTargetFeaturesView.addOnItemTouchListener(listener);
}
@@ -307,8 +312,11 @@ class MenuView extends FrameLayout implements
void updateMenuMoveToTucked(boolean isMoveToTucked) {
mIsMoveToTucked = isMoveToTucked;
mMenuViewModel.updateMenuMoveToTucked(isMoveToTucked);
+ if (mMoveToTuckedListener != null) {
+ mMoveToTuckedListener.onMoveToTuckedChanged(isMoveToTucked);
+ }
- if (Flags.floatingMenuOverlapsNavBarsFlag()) {
+ if (Flags.floatingMenuOverlapsNavBarsFlag() && !Flags.floatingMenuAnimatedTuck()) {
if (isMoveToTucked) {
final float halfWidth = getMenuWidth() / 2.0f;
final boolean isOnLeftSide = mMenuAnimationController.isOnLeftSide();
@@ -428,4 +436,11 @@ class MenuView extends FrameLayout implements
*/
void onChange(List<AccessibilityTarget> newTargetFeatures);
}
+
+ /**
+ * Interface containing a callback for when MoveToTucked changes.
+ */
+ interface OnMoveToTuckedListener {
+ void onMoveToTuckedChanged(boolean moveToTucked);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
index 89ce06514e1c..4865fcedc457 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
@@ -281,7 +281,7 @@ class MenuViewAppearance {
: new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
}
- private Rect getWindowAvailableBounds() {
+ public Rect getWindowAvailableBounds() {
final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
final WindowInsets windowInsets = windowMetrics.getWindowInsets();
final Insets insets = windowInsets.getInsetsIgnoringVisibility(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index fbca02290236..ff3a9e3bd409 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -79,7 +79,8 @@ import java.util.Optional;
*/
@SuppressLint("ViewConstructor")
class MenuViewLayer extends FrameLayout implements
- ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks {
+ ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks,
+ MenuView.OnMoveToTuckedListener {
private static final int SHOW_MESSAGE_DELAY_MS = 3000;
private final WindowManager mWindowManager;
@@ -211,6 +212,7 @@ class MenuViewLayer extends FrameLayout implements
mMenuListViewTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
mDismissAnimationController);
mMenuView.addOnItemTouchListenerToList(mMenuListViewTouchHandler);
+ mMenuView.setMoveToTuckedListener(this);
mMessageView = new MenuMessageView(context);
@@ -232,6 +234,10 @@ class MenuViewLayer extends FrameLayout implements
addView(mMenuView, LayerIndex.MENU_VIEW);
addView(mDismissView, LayerIndex.DISMISS_VIEW);
addView(mMessageView, LayerIndex.MESSAGE_VIEW);
+
+ if (Flags.floatingMenuAnimatedTuck()) {
+ setClipChildren(true);
+ }
}
@Override
@@ -354,6 +360,24 @@ class MenuViewLayer extends FrameLayout implements
mShouldShowDockTooltip = !hasSeenTooltip;
}
+ public void onMoveToTuckedChanged(boolean moveToTuck) {
+ if (Flags.floatingMenuOverlapsNavBarsFlag()) {
+ if (moveToTuck) {
+ final Rect bounds = mMenuViewAppearance.getWindowAvailableBounds();
+ final int[] location = getLocationOnScreen();
+ bounds.offset(
+ location[0],
+ location[1]
+ );
+
+ setClipBounds(bounds);
+ }
+ // Instead of clearing clip bounds when moveToTuck is false,
+ // wait until the spring animation finishes.
+ }
+ // Function is a no-operation if flag is disabled.
+ }
+
private void onSpringAnimationsEndAction() {
if (mShouldShowDockTooltip) {
mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
@@ -364,6 +388,11 @@ class MenuViewLayer extends FrameLayout implements
mMenuAnimationController.startTuckedAnimationPreview();
}
+ if (Flags.floatingMenuAnimatedTuck()) {
+ if (!mMenuView.isMoveToTucked()) {
+ setClipBounds(null);
+ }
+ }
if (Flags.floatingMenuImeDisplacementAnimation()) {
mMenuView.onArrivalAtPosition();
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
index b2bc06f0ae29..48d374207388 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
@@ -20,4 +20,7 @@ package com.android.systemui.common.shared.model
data class SharedNotificationContainerPosition(
val top: Float = 0f,
val bottom: Float = 0f,
+
+ /** Whether any modifications to top/bottom are smoothly animated */
+ val animate: Boolean = false,
)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index be9a2993dab1..42bb5bb2a361 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -77,7 +77,7 @@ import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.asIndenting
import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.indentIfPossible
+import com.android.systemui.util.withIncreasedIndent
import com.android.wm.shell.taskview.TaskViewFactory
import dagger.Lazy
import java.io.PrintWriter
@@ -822,9 +822,9 @@ class ControlsUiControllerImpl @Inject constructor (
private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? =
items.firstOrNull { it.matches(si) }
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("ControlsUiControllerImpl:")
- pw.asIndenting().indentIfPossible {
+ override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ println("ControlsUiControllerImpl:")
+ withIncreasedIndent {
println("hidden: $hidden")
println("selectedItem: $selectedItem")
println("lastSelections: $lastSelections")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 6a0d5954fc44..c4dfe9afeb2a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -383,7 +383,12 @@ constructor(
"isFaceAuthEnrolledAndEnabled"
),
Pair(keyguardRepository.isKeyguardGoingAway.isFalse(), "keyguardNotGoingAway"),
- Pair(powerInteractor.isAsleep.isFalse(), "deviceNotAsleep"),
+ Pair(
+ keyguardTransitionInteractor
+ .isInTransitionToStateWhere(KeyguardState::deviceIsAsleepInState)
+ .isFalse(),
+ "deviceNotTransitioningToAsleepState"
+ ),
Pair(
keyguardInteractor.isSecureCameraActive
.isFalse()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 3eef6aa37122..8d5d73f88ca1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -115,7 +115,8 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio
private var updateTransitionId: UUID? = null
init {
- // Seed with transitions signaling a boot into lockscreen state
+ // Seed with transitions signaling a boot into lockscreen state. If updating this, please
+ // also update FakeKeyguardTransitionRepository.
emitTransition(
TransitionStep(
KeyguardState.OFF,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 4d5c503d1c4e..67a12b06de0f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -22,6 +22,8 @@ import android.view.View
import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import android.view.ViewGroup.OnHierarchyChangeListener
+import android.view.WindowInsets
+import android.view.WindowInsets.Type
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.internal.jank.InteractionJankMonitor
@@ -242,11 +244,18 @@ object KeyguardRootViewBinder {
}
)
+ view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
+ val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
+ viewModel.topInset = insets.getInsetsIgnoringVisibility(insetTypes).top
+ insets
+ }
+
return object : DisposableHandle {
override fun dispose() {
disposableHandle.dispose()
view.removeOnLayoutChangeListener(onLayoutChangeListener)
view.setOnHierarchyChangeListener(null)
+ view.setOnApplyWindowInsetsListener(null)
childViews.clear()
}
}
@@ -288,7 +297,6 @@ object KeyguardRootViewBinder {
oldBottom: Int
) {
val nsslPlaceholder = v.findViewById(R.id.nssl_placeholder) as View?
-
if (nsslPlaceholder != null) {
// After layout, ensure the notifications are positioned correctly
viewModel.onSharedNotificationContainerPositionChanged(
@@ -296,6 +304,11 @@ object KeyguardRootViewBinder {
nsslPlaceholder.bottom.toFloat(),
)
}
+
+ val ksv = v.findViewById(R.id.keyguard_status_view) as View?
+ if (ksv != null) {
+ viewModel.statusViewTop = ksv.top
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 1f98082c4065..e12da53287ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -65,7 +65,12 @@ constructor(
*/
private val previewMode = MutableStateFlow(PreviewMode())
- public var clockControllerProvider: Provider<ClockController>? = null
+ var clockControllerProvider: Provider<ClockController>? = null
+
+ /** System insets that keyguard needs to stay out of */
+ var topInset: Int = 0
+ /** Status view top, without translation added in */
+ var statusViewTop: Int = 0
val burnInLayerVisibility: Flow<Int> =
keyguardTransitionInteractor.startedKeyguardState
@@ -102,9 +107,12 @@ constructor(
scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
)
} else {
+ // Ensure the desired translation doesn't encroach on the top inset
+ val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt()
+ val translationY = -(statusViewTop - Math.max(topInset, statusViewTop + burnInY))
BurnInModel(
translationX = MathUtils.lerp(0, burnIn.translationX, interpolation).toInt(),
- translationY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt(),
+ translationY = translationY,
scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
scaleClockOnly = true,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 0190d5c9759b..2cd55609a749 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -61,7 +61,9 @@ import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.util.asIndenting
import com.android.systemui.util.concurrency.Execution
+import com.android.systemui.util.printCollection
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.time.SystemClock
import java.io.PrintWriter
@@ -587,10 +589,9 @@ constructor(
return null
}
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("Region Samplers: ${regionSamplers.size}")
- regionSamplers.map { (_, sampler) ->
- sampler.dump(pw)
+ override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ printCollection("Region Samplers", regionSamplers.values) {
+ it.dump(this)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt
index 9a93abd463c7..b200136b1b43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt
@@ -28,6 +28,10 @@ import com.android.systemui.statusbar.notification.collection.render.NotifGutsVi
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager
import com.android.systemui.statusbar.notification.row.NotificationGuts
import com.android.systemui.statusbar.notification.row.NotificationGutsManager
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.println
+import com.android.systemui.util.withIncreasedIndent
import java.io.PrintWriter
import javax.inject.Inject
@@ -54,7 +58,7 @@ class GutsCoordinator @Inject constructor(
private var onEndLifetimeExtensionCallback: OnEndLifetimeExtensionCallback? = null
init {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerDumpable(this)
}
override fun attach(pipeline: NotifPipeline) {
@@ -62,16 +66,12 @@ class GutsCoordinator @Inject constructor(
pipeline.addNotificationLifetimeExtender(mLifetimeExtender)
}
- override fun dump(pw: PrintWriter, args: Array<String>) {
- pw.println(" notifsWithOpenGuts: ${notifsWithOpenGuts.size}")
- for (key in notifsWithOpenGuts) {
- pw.println(" * $key")
+ override fun dump(pw: PrintWriter, args: Array<String>) = pw.asIndenting().run {
+ withIncreasedIndent {
+ printCollection("notifsWithOpenGuts", notifsWithOpenGuts)
+ printCollection("notifsExtendingLifetime", notifsExtendingLifetime)
+ println("onEndLifetimeExtensionCallback", onEndLifetimeExtensionCallback)
}
- pw.println(" notifsExtendingLifetime: ${notifsExtendingLifetime.size}")
- for (key in notifsExtendingLifetime) {
- pw.println(" * $key")
- }
- pw.println(" onEndLifetimeExtensionCallback: $onEndLifetimeExtensionCallback")
}
private val mLifetimeExtender: NotifLifetimeExtender = object : NotifLifetimeExtender {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
index 8bce5b011d0c..5e5f2a12cdf3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
@@ -20,6 +20,8 @@ import android.service.notification.NotificationListenerService.RankingMap
import android.util.ArrayMap
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
import java.io.PrintWriter
class NotifCollectionInconsistencyTracker(val logger: NotifCollectionLogger) {
@@ -104,15 +106,9 @@ class NotifCollectionInconsistencyTracker(val logger: NotifCollectionLogger) {
}
}
- fun dump(pw: PrintWriter) {
- pw.println("notificationsWithoutRankings: ${notificationsWithoutRankings.size}")
- for (key in notificationsWithoutRankings) {
- pw.println("\t * : $key")
- }
- pw.println("missingNotifications: ${missingNotifications.size}")
- for (key in missingNotifications) {
- pw.println("\t * : $key")
- }
+ fun dump(pw: PrintWriter) = pw.asIndenting().run {
+ printCollection("notificationsWithoutRankings", notificationsWithoutRankings)
+ printCollection("missingNotifications", missingNotifications)
}
private var attached: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
index febc011043a0..7b0a28a0ef8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
@@ -5,6 +5,10 @@ import android.util.ArrayMap
import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.println
+import com.android.systemui.util.withIncreasedIndent
import java.io.PrintWriter
/**
@@ -104,9 +108,10 @@ abstract class SelfTrackingLifetimeExtender(
mCallback = callback
}
- final override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("LifetimeExtender: $name:")
- pw.println(" mEntriesExtended: ${mEntriesExtended.size}")
- mEntriesExtended.forEach { pw.println(" * ${it.key}") }
+ final override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ println("LifetimeExtender", name)
+ withIncreasedIndent {
+ printCollection("mEntriesExtended", mEntriesExtended.keys)
+ }
}
} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
index c873e6ad36d4..58712bf8e304 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
@@ -26,6 +26,9 @@ import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.util.Assert
import com.android.systemui.util.ListenerSet
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.println
import java.io.PrintWriter
import javax.inject.Inject
@@ -86,12 +89,9 @@ class DebugModeFilterProvider @Inject constructor(
return entry.sbn.packageName !in allowedPackages
}
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("initialized: ${listeners.isNotEmpty()}")
- pw.println("allowedPackages: ${allowedPackages.size}")
- allowedPackages.forEachIndexed { i, pkg ->
- pw.println(" [$i]: $pkg")
- }
+ override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ println("initialized", listeners.isNotEmpty())
+ printCollection("allowedPackages", allowedPackages)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
index 78e9a740a547..9326d3385d65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
@@ -22,7 +22,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.util.asIndenting
-import com.android.systemui.util.withIncreasedIndent
+import com.android.systemui.util.printCollection
import java.io.PrintWriter
import javax.inject.Inject
@@ -49,11 +49,7 @@ class NotificationDismissibilityProviderImpl @Inject constructor(dumpManager: Du
}
override fun dump(pw: PrintWriter, args: Array<out String>) =
- pw.asIndenting().run {
- println("non-dismissible entries: ${nonDismissableEntryKeys.size}")
-
- withIncreasedIndent { nonDismissableEntryKeys.forEach(this::println) }
- }
+ pw.asIndenting().run { printCollection("non-dismissible entries", nonDismissableEntryKeys) }
companion object {
private const val TAG = "NotificationDismissibilityProvider"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index db7f46eb28f2..aca8b64c05d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -26,6 +26,7 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.ListenerSet
import com.android.systemui.util.asIndenting
+import com.android.systemui.util.println
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.withIncreasedIndent
@@ -229,13 +230,13 @@ class KeyguardNotificationVisibilityProviderImpl @Inject constructor(
}
override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
- println("isLockedOrLocking=$isLockedOrLocking")
+ println("isLockedOrLocking", isLockedOrLocking)
withIncreasedIndent {
- println("keyguardStateController.isShowing=${keyguardStateController.isShowing}")
- println("statusBarStateController.currentOrUpcomingState=" +
- "${statusBarStateController.currentOrUpcomingState}")
+ println("keyguardStateController.isShowing", keyguardStateController.isShowing)
+ println("statusBarStateController.currentOrUpcomingState",
+ statusBarStateController.currentOrUpcomingState)
}
- println("hideSilentNotificationsOnLockscreen=$hideSilentNotificationsOnLockscreen")
+ println("hideSilentNotificationsOnLockscreen", hideSilentNotificationsOnLockscreen)
}
private val isLockedOrLocking get() =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 60e75ff9e6e1..6528cef3dec1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -118,6 +118,7 @@ public class NotificationContentView extends FrameLayout implements Notification
private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper;
private NotificationViewWrapper mHeadsUpWrapper;
+ @Nullable private NotificationViewWrapper mShownWrapper = null;
private final HybridGroupManager mHybridGroupManager;
private int mClipTopAmount;
private int mContentHeight;
@@ -417,6 +418,8 @@ public class NotificationContentView extends FrameLayout implements Notification
mContractedChild = child;
mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
mContainingNotification);
+ // The contracted wrapper has changed. If this is the shown wrapper, we need to update it.
+ updateShownWrapper(mVisibleType);
}
private NotificationViewWrapper getWrapperForView(View child) {
@@ -480,6 +483,8 @@ public class NotificationContentView extends FrameLayout implements Notification
if (mContainingNotification != null) {
applySystemActions(mExpandedChild, mContainingNotification.getEntry());
}
+ // The expanded wrapper has changed. If this is the shown wrapper, we need to update it.
+ updateShownWrapper(mVisibleType);
}
/**
@@ -530,6 +535,8 @@ public class NotificationContentView extends FrameLayout implements Notification
if (mContainingNotification != null) {
applySystemActions(mHeadsUpChild, mContainingNotification.getEntry());
}
+ // The heads up wrapper has changed. If this is the shown wrapper, we need to update it.
+ updateShownWrapper(mVisibleType);
}
@Override
@@ -886,6 +893,7 @@ public class NotificationContentView extends FrameLayout implements Notification
forceUpdateVisibility(VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper);
forceUpdateVisibility(VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper);
forceUpdateVisibility(VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView);
+ updateShownWrapper(mVisibleType);
fireExpandedVisibleListenerIfVisible();
// forceUpdateVisibilities cancels outstanding animations without updating the
// mAnimationStartVisibleType. Do so here instead.
@@ -967,6 +975,7 @@ public class NotificationContentView extends FrameLayout implements Notification
mHeadsUpChild, mHeadsUpWrapper);
updateViewVisibility(visibleType, VISIBLE_TYPE_SINGLELINE,
mSingleLineView, mSingleLineView);
+ updateShownWrapper(visibleType);
fireExpandedVisibleListenerIfVisible();
// updateViewVisibilities cancels outstanding animations without updating the
// mAnimationStartVisibleType. Do so here instead.
@@ -980,6 +989,28 @@ public class NotificationContentView extends FrameLayout implements Notification
}
}
+ /**
+ * Called when the currently shown wrapper is potentially affected by a change to the
+ * {mVisibleType} or the user-visibility of this view.
+ *
+ * @see View#isShown()
+ */
+ private void updateShownWrapper(int visibleType) {
+ final NotificationViewWrapper shownWrapper = isShown() ? getVisibleWrapper(visibleType)
+ : null;
+
+ if (mShownWrapper != shownWrapper) {
+ NotificationViewWrapper hiddenWrapper = mShownWrapper;
+ mShownWrapper = shownWrapper;
+ if (hiddenWrapper != null) {
+ hiddenWrapper.onContentShown(false);
+ }
+ if (shownWrapper != null) {
+ shownWrapper.onContentShown(true);
+ }
+ }
+ }
+
private void animateToVisibleType(int visibleType) {
final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
@@ -990,6 +1021,7 @@ public class NotificationContentView extends FrameLayout implements Notification
mAnimationStartVisibleType = mVisibleType;
shownView.transformFrom(hiddenView);
getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
+ updateShownWrapper(visibleType);
hiddenView.transformTo(shownView, new Runnable() {
@Override
public void run() {
@@ -1837,6 +1869,7 @@ public class NotificationContentView extends FrameLayout implements Notification
@Override
public void onVisibilityAggregated(boolean isVisible) {
super.onVisibilityAggregated(isVisible);
+ updateShownWrapper(mVisibleType);
if (isVisible) {
fireExpandedVisibleListenerIfVisible();
}
@@ -2217,6 +2250,21 @@ public class NotificationContentView extends FrameLayout implements Notification
}
@VisibleForTesting
+ protected NotificationViewWrapper getContractedWrapper() {
+ return mContractedWrapper;
+ }
+
+ @VisibleForTesting
+ protected NotificationViewWrapper getExpandedWrapper() {
+ return mExpandedWrapper;
+ }
+
+ @VisibleForTesting
+ protected NotificationViewWrapper getHeadsUpWrapper() {
+ return mHeadsUpWrapper;
+ }
+
+ @VisibleForTesting
protected void setContractedWrapper(NotificationViewWrapper contractedWrapper) {
mContractedWrapper = contractedWrapper;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
index acd6cc69b553..990adf77a902 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
@@ -68,13 +68,11 @@ public class NotificationBigPictureTemplateViewWrapper extends NotificationTempl
}
@Override
- public void setVisible(boolean visible) {
- super.setVisible(visible);
-
+ public void onContentShown(boolean shown) {
+ super.onContentShown(shown);
BigPictureIconManager imageManager = mRow.getBigPictureIconManager();
if (imageManager != null) {
- // TODO(b/283082473) call it a bit earlier for true, as soon as the row starts to expand
- imageManager.onViewShown(visible);
+ imageManager.onViewShown(shown);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index cdf178e813f5..50f3e7896442 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -311,6 +311,17 @@ public abstract class NotificationViewWrapper implements TransformableView {
}
/**
+ * Called when the user-visibility of this content wrapper has changed.
+ *
+ * @param shown true if the content of this wrapper is user-visible, meaning that the wrapped
+ * view and all of its ancestors are visible.
+ *
+ * @see View#isShown()
+ */
+ public void onContentShown(boolean shown) {
+ }
+
+ /**
* Called to indicate this view is removed
*/
public void setRemoved() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 8babcc24043a..1774000d8f87 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -4963,7 +4963,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
// Avoid Flicking during clear all
// when the shade finishes closing, onExpansionStopped will call
// resetScrollPosition to setOwnScrollY to 0
- if (mAmbientState.isClosing()) {
+ if (mAmbientState.isClosing() || mAmbientState.isClearAllInProgress()) {
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 2af7181f2f31..6785da4bf4f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -59,10 +59,8 @@ object SharedNotificationContainerBinder {
launch {
viewModel.position.collect {
- controller.updateTopPadding(
- it.top,
- controller.isAddOrRemoveAnimationPending()
- )
+ val animate = it.animate || controller.isAddOrRemoveAnimationPending()
+ controller.updateTopPadding(it.top, animate)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 1229cb9b49ac..b86b5dcc7939 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -25,6 +25,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -118,8 +119,15 @@ constructor(
}
}
} else {
- interactor.topPosition.map { top ->
- keyguardInteractor.sharedNotificationContainerPosition.value.copy(top = top)
+ interactor.topPosition.sample(shadeInteractor.qsExpansion, ::Pair).map {
+ (top, qsExpansion) ->
+ // When QS expansion > 0, it should directly set the top padding so do not
+ // animate it
+ val animate = qsExpansion == 0f
+ keyguardInteractor.sharedNotificationContainerPosition.value.copy(
+ top = top,
+ animate = animate
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt b/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
index 018ef968ff2d..5b0943a77979 100644
--- a/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
@@ -18,6 +18,7 @@ package com.android.systemui.util
import android.util.IndentingPrintWriter
import android.view.View
+import com.android.systemui.Dumpable
import java.io.PrintWriter
/**
@@ -56,13 +57,28 @@ fun IndentingPrintWriter.withIncreasedIndent(runnable: Runnable) {
}
/** Print a line which is '$label=$value' */
-fun IndentingPrintWriter.println(label: String, value: Any) =
+fun IndentingPrintWriter.println(label: String, value: Any?) =
append(label).append('=').println(value)
-/** Return a readable string for the visibility */
-fun visibilityString(@View.Visibility visibility: Int): String = when (visibility) {
- View.GONE -> "gone"
- View.VISIBLE -> "visible"
- View.INVISIBLE -> "invisible"
- else -> "unknown:$visibility"
+@JvmOverloads
+inline fun <T> IndentingPrintWriter.printCollection(
+ label: String,
+ collection: Collection<T>,
+ printer: IndentingPrintWriter.(T) -> Unit = IndentingPrintWriter::println,
+) {
+ append(label).append(": ").println(collection.size)
+ withIncreasedIndent { collection.forEach { printer(it) } }
+}
+
+fun <T : Dumpable> IndentingPrintWriter.dumpCollection(label: String, collection: Collection<T>) {
+ printCollection(label, collection) { it.dump(this, emptyArray()) }
}
+
+/** Return a readable string for the visibility */
+fun visibilityString(@View.Visibility visibility: Int): String =
+ when (visibility) {
+ View.GONE -> "gone"
+ View.VISIBLE -> "visible"
+ View.INVISIBLE -> "invisible"
+ else -> "unknown:$visibility"
+ }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 284c273fa831..728102d806fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -241,9 +241,13 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
throws RemoteException {
enableWindowMagnificationWithoutAnimation();
+ // Wait for Rects updated.
+ waitForIdleSync();
+ View mirrorView = mWindowManager.getAttachedView();
final float targetScale = 1.0f;
- final float targetCenterX = DEFAULT_CENTER_X + 100;
- final float targetCenterY = DEFAULT_CENTER_Y + 100;
+ // Move the magnifier to the top left corner, within the boundary
+ final float targetCenterX = mirrorView.getWidth() / 2.0f;
+ final float targetCenterY = mirrorView.getHeight() / 2.0f;
Mockito.reset(mSpyController);
mInstrumentation.runOnMainSync(() -> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index 2e75480a7391..834dccbd2979 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -26,6 +26,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import android.graphics.PointF;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
@@ -39,6 +42,7 @@ import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.test.filters.SmallTest;
+import com.android.systemui.Flags;
import com.android.systemui.Prefs;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.settings.SecureSettings;
@@ -70,6 +74,10 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Mock
private AccessibilityManager mAccessibilityManager;
@@ -223,6 +231,24 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
verifyZeroInteractions(onSpringAnimationsEndCallback);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK)
+ public void tuck_animates() {
+ mMenuAnimationController.cancelAnimations();
+ mMenuAnimationController.moveToEdgeAndHide();
+ assertThat(mMenuAnimationController.getAnimation(
+ DynamicAnimation.TRANSLATION_X).isRunning()).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK)
+ public void untuck_animates() {
+ mMenuAnimationController.cancelAnimations();
+ mMenuAnimationController.moveOutEdgeAndShow();
+ assertThat(mMenuAnimationController.getAnimation(
+ DynamicAnimation.TRANSLATION_X).isRunning()).isTrue();
+ }
+
private void setupAndRunSpringAnimations() {
final float stiffness = 700f;
final float dampingRatio = 0.85f;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 00951c30fbe5..f0ff77ebf1ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -12,7 +12,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepos
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
@@ -45,7 +44,6 @@ class ResourceTrimmerTest : SysuiTestCase() {
private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
private lateinit var powerInteractor: PowerInteractor
-
@Mock private lateinit var globalWindowManager: GlobalWindowManager
private lateinit var resourceTrimmer: ResourceTrimmer
@@ -181,8 +179,10 @@ class ResourceTrimmerTest : SysuiTestCase() {
@Test
fun keyguardTransitionsToGone_trimsFontCache() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
verify(globalWindowManager, times(1))
.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
@@ -194,8 +194,10 @@ class ResourceTrimmerTest : SysuiTestCase() {
fun keyguardTransitionsToGone_flagDisabled_doesNotTrimFontCache() =
testScope.runTest {
featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
// Memory hidden should still be called.
verify(globalWindowManager, times(1))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 9bb2434f84ac..8d9bc751fbc9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -32,7 +32,6 @@ import android.hardware.face.FaceManager
import android.hardware.face.FaceSensorProperties
import android.hardware.face.FaceSensorPropertiesInternal
import android.os.CancellationSignal
-import android.util.Log
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -637,7 +636,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
@Test
fun authenticateDoesNotRunWhenDeviceIsGoingToSleep() =
- testScope.runTest { testGatingCheckForFaceAuth { powerInteractor.setAsleepForTest() } }
+ testScope.runTest {
+ testGatingCheckForFaceAuth {
+ powerInteractor.setAsleepForTest()
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ )
+ )
+ runCurrent()
+ }
+ }
@Test
fun authenticateDoesNotRunWhenSecureCameraIsActive() =
@@ -733,13 +744,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
allPreconditionsToRunFaceAuthAreTrue()
- Log.i("TEST", "started waking")
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.OFF,
- transitionState = TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OFF,
+ testScope
)
runCurrent()
keyguardTransitionRepository.sendTransitionStep(
@@ -751,15 +759,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
)
runCurrent()
- Log.i("TEST", "sending display off")
displayRepository.emit(setOf(display(0, 0, Display.DEFAULT_DISPLAY, Display.STATE_OFF)))
displayRepository.emitDisplayChangeEvent(Display.DEFAULT_DISPLAY)
- Log.i("TEST", "sending step")
-
runCurrent()
- Log.i("TEST", "About to assert if face auth can run.")
assertThat(canFaceAuthRun()).isTrue()
}
@@ -768,12 +772,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
testScope.runTest {
testGatingCheckForFaceAuth {
powerInteractor.onFinishedWakingUp()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.OFF,
- to = KeyguardState.LOCKSCREEN,
- transitionState = TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
)
runCurrent()
@@ -923,7 +925,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
@Test
fun detectDoesNotRunWhenDeviceSleepingStartingToSleep() =
- testScope.runTest { testGatingCheckForDetect { powerInteractor.setAsleepForTest() } }
+ testScope.runTest {
+ testGatingCheckForDetect {
+ powerInteractor.setAsleepForTest()
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ )
+ )
+ runCurrent()
+ }
+ }
@Test
fun detectDoesNotRunWhenSecureCameraIsActive() =
@@ -1016,14 +1030,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromDozing() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.DOZING,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1031,14 +1042,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromAod() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.AOD,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1046,14 +1054,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromLockscreen() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1061,14 +1066,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromBouncer() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.PRIMARY_BOUNCER,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1251,6 +1253,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
keyguardRepository.setKeyguardShowing(true)
displayRepository.emit(setOf(display(0, 0, Display.DEFAULT_DISPLAY, Display.STATE_ON)))
displayRepository.emitDisplayChangeEvent(Display.DEFAULT_DISPLAY)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
+ )
runCurrent()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index e87adf5e424b..e75f5570248e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -26,8 +26,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepos
import com.android.systemui.keyguard.shared.model.DismissAction
import com.android.systemui.keyguard.shared.model.KeyguardDone
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -179,8 +177,10 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() {
assertThat(executeDismissAction).isNull()
// WHEN the keyguard is GONE
- transitionRepository.sendTransitionStep(
- TransitionStep(to = KeyguardState.GONE, transitionState = TransitionState.FINISHED)
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
assertThat(executeDismissAction).isNotNull()
}
@@ -198,11 +198,10 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() {
willAnimateOnLockscreen = true,
)
)
- transitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.AOD,
- transitionState = TransitionState.FINISHED,
- )
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ testScope
)
assertThat(resetDismissAction).isEqualTo(Unit)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
index 0c74a38fea04..98f0211587ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -21,7 +21,6 @@ import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
@@ -29,7 +28,7 @@ import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -265,17 +264,17 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
underTest.onLongPress()
assertThat(isMenuVisible).isTrue()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.GONE,
- ),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
assertThat(isMenuVisible).isFalse()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.LOCKSCREEN,
- ),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
)
assertThat(isMenuVisible).isFalse()
}
@@ -312,10 +311,10 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
keyguardState: KeyguardState = KeyguardState.LOCKSCREEN,
isQuickSettingsVisible: Boolean = false,
) {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = keyguardState,
- ),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = keyguardState,
+ testScope = testScope
)
keyguardRepository.setQuickSettingsVisible(isVisible = isQuickSettingsVisible)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index 16f2fa22d5fb..6eed427e9297 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -231,12 +231,10 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
surfaceBehindIsAnimatingFlow.emit(true)
runCurrent()
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.FINISHED,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- )
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index 3efe38295f3d..a04ea2e4fa9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -72,7 +72,7 @@ class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
onFinish = { 10f },
)
var animationValues = collectLastValue(flow)
- repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED), validateStep = false)
assertThat(animationValues()).isEqualTo(10f)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 4f545cb0e288..b80771ff646c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -179,6 +179,8 @@ class KeyguardRootViewModelTest : SysuiTestCase() {
val translationY by collectLastValue(underTest.translationY)
val scale by collectLastValue(underTest.scale)
+ underTest.statusViewTop = 100
+
// Set to dozing (on AOD)
dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
// Trigger a change to the burn-in model
@@ -200,6 +202,37 @@ class KeyguardRootViewModelTest : SysuiTestCase() {
}
@Test
+ fun translationAndScaleFromBurnFullyDozingStaysOutOfTopInset() =
+ testScope.runTest {
+ val translationX by collectLastValue(underTest.translationX)
+ val translationY by collectLastValue(underTest.translationY)
+ val scale by collectLastValue(underTest.scale)
+
+ underTest.statusViewTop = 100
+ underTest.topInset = 80
+
+ // Set to dozing (on AOD)
+ dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
+ // Trigger a change to the burn-in model
+ burnInFlow.value =
+ BurnInModel(
+ translationX = 20,
+ translationY = -30,
+ scale = 0.5f,
+ )
+ assertThat(translationX).isEqualTo(20)
+ // -20 instead of -30, due to inset of 80
+ assertThat(translationY).isEqualTo(-20)
+ assertThat(scale).isEqualTo(Pair(0.5f, true /* scaleClockOnly */))
+
+ // Set to the beginning of GONE->AOD transition
+ goneToAodTransitionStep.emit(TransitionStep(value = 0f))
+ assertThat(translationX).isEqualTo(0)
+ assertThat(translationY).isEqualTo(0)
+ assertThat(scale).isEqualTo(Pair(1f, true /* scaleClockOnly */))
+ }
+
+ @Test
fun translationAndScaleFromBurnInUseScaleOnly() =
testScope.runTest {
whenever(clockController.config.useAlternateSmartspaceAODTransition).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
index edcaa1d65f49..30e48669205f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
@@ -751,14 +751,10 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() {
}
private suspend fun givenTransitionToLockscreenFinished() {
- transitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
- value = 1f,
- transitionState = TransitionState.FINISHED,
- ownerName = "givenTransitionToLockscreenFinished",
- )
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index a4c2a0850ce4..3bfdb84249ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -31,7 +31,6 @@ import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.keyguard.TestScopeProvider
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
@@ -39,8 +38,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepos
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
@@ -52,6 +49,7 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
+import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -810,8 +808,10 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.mediaCarousel = mediaCarousel
val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this)
- transitionRepository.sendTransitionStep(
- TransitionStep(to = KeyguardState.GONE, transitionState = TransitionState.FINISHED)
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this
)
verify(mediaCarousel).visibility = View.VISIBLE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index 6c1f537e754f..2ee016b2c87c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -28,8 +28,6 @@ import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
@@ -51,8 +49,6 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -66,6 +62,8 @@ import org.mockito.Mockito.anyString
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import java.util.function.Consumer
+import kotlin.time.Duration.Companion.seconds
import org.mockito.Mockito.`when` as whenever
@SmallTest
@@ -131,8 +129,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
collectionListener.onEntryAdded(fakeEntry)
// WHEN: The device transitions to AOD
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -147,8 +147,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
keyguardRepository.setKeyguardShowing(false)
whenever(statusBarStateController.isExpanded).thenReturn(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
// WHEN: A notification is posted
@@ -161,8 +163,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// WHEN: The keyguard is now showing
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -171,8 +175,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// WHEN: The keyguard goes away
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -337,8 +343,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
runKeyguardCoordinatorTest {
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -348,15 +356,19 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// WHEN: Keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -370,16 +382,20 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// GIVEN: Keyguard is showing, unseen notification is present
keyguardRepository.setKeyguardShowing(true)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
// WHEN: Keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
// WHEN: Keyguard is shown again
@@ -397,8 +413,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
val firstEntry = NotificationEntryBuilder().setId(1).build()
collectionListener.onEntryAdded(firstEntry)
@@ -419,15 +437,19 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -445,8 +467,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -473,15 +497,19 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -496,8 +524,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -524,15 +554,19 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -547,8 +581,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -571,15 +607,19 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
index 41c7071a616d..14d188c69525 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
@@ -370,11 +370,10 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() {
scope.runTest {
val isVisible by collectLastValue(underTest.isVisible)
runCurrent()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.GONE,
+ scope,
)
whenever(screenOffAnimController.shouldShowAodIconsWhenShade()).thenReturn(false)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index c4baa691e612..5549feefe090 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -16,13 +16,17 @@
package com.android.systemui.statusbar.notification.row
+import android.annotation.DimenRes
import android.content.res.Resources
import android.os.UserHandle
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.ViewUtils
import android.view.NotificationHeaderView
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
@@ -30,20 +34,21 @@ import com.android.internal.R
import com.android.internal.widget.NotificationActionListLayout
import com.android.internal.widget.NotificationExpandButton
import com.android.systemui.SysuiTestCase
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.statusbar.notification.FeedbackIcon
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
-import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
@@ -53,89 +58,247 @@ import org.mockito.MockitoAnnotations.initMocks
@SmallTest
@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
class NotificationContentViewTest : SysuiTestCase() {
- private lateinit var view: NotificationContentView
+ private lateinit var row: ExpandableNotificationRow
+ private lateinit var fakeParent: ViewGroup
@Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier
- private val notificationContentMargin =
- mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin)
+ private val testableResources = mContext.getOrCreateTestableResources()
+ private val contractedHeight =
+ px(com.android.systemui.res.R.dimen.min_notification_layout_height)
+ private val expandedHeight = px(com.android.systemui.res.R.dimen.notification_max_height)
+ private val notificationContentMargin = px(R.dimen.notification_content_margin)
@Before
fun setup() {
initMocks(this)
+ fakeParent = FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE }
+ row =
+ spy(
+ ExpandableNotificationRow(mContext, /* attrs= */ null).apply {
+ entry = createMockNotificationEntry()
+ }
+ )
+ ViewUtils.attachView(fakeParent)
+ }
- mDependency.injectMockDependency(MediaOutputDialogFactory::class.java)
-
- view = spy(NotificationContentView(mContext, /* attrs= */ null))
- val row = ExpandableNotificationRow(mContext, /* attrs= */ null)
- row.entry = createMockNotificationEntry(false)
- val spyRow = spy(row)
- doReturn(10).whenever(spyRow).intrinsicHeight
-
- with(view) {
- initialize(mPeopleNotificationIdentifier, mock(), mock(), mock(), mock())
- setContainingNotification(spyRow)
- setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30)
- contractedChild = createViewWithHeight(10)
- expandedChild = createViewWithHeight(20)
- headsUpChild = createViewWithHeight(30)
- measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
- layout(0, 0, view.measuredWidth, view.measuredHeight)
- }
+ @After
+ fun teardown() {
+ fakeParent.removeAllViews()
+ ViewUtils.detachView(fakeParent)
}
- private fun createViewWithHeight(height: Int) =
- View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+ @Test
+ fun contractedWrapperSelected_whenShadeIsClosed_wrapperNotNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+
+ // WHEN a collapsed content is created
+ val view = createContentView(isSystemExpanded = false)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is visible, but NOT shown
+ verify(view.contractedWrapper).setVisible(true)
+ verify(view.contractedWrapper, never()).onContentShown(anyBoolean())
+ }
+
+ @Test
+ fun contractedWrapperSelected_whenShadeIsOpen_wrapperNotified() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+
+ // WHEN a collapsed content is created
+ val view = createContentView(isSystemExpanded = false)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is visible and shown
+ verify(view.contractedWrapper, Mockito.atLeastOnce()).setVisible(true)
+ verify(view.contractedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeOpens_collapsedWrapperIsSelected_wrapperNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+ // AND a collapsed content is created
+ val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.VISIBLE
+ view.onVisibilityAggregated(true)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is shown
+ verify(view.contractedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeCloses_collapsedWrapperIsShown_wrapperNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.VISIBLE
+ // AND a collapsed content is created
+ val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.GONE
+ view.onVisibilityAggregated(false)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is NOT shown
+ verify(view.contractedWrapper, times(1)).onContentShown(false)
+ }
+
+ @Test
+ fun expandedWrapperSelected_whenShadeIsClosed_wrapperNotNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+
+ // WHEN a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is visible, but NOT shown
+ verify(view.expandedWrapper, Mockito.atLeastOnce()).setVisible(true)
+ verify(view.expandedWrapper, never()).onContentShown(anyBoolean())
+ }
+
+ @Test
+ fun expandedWrapperSelected_whenShadeIsOpen_wrapperNotified() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+
+ // WHEN an system-expanded content is created
+ val view = createContentView(isSystemExpanded = true)
+
+ // THEN the expandedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the expandedWrapper is visible and shown
+ verify(view.expandedWrapper, Mockito.atLeastOnce()).setVisible(true)
+ verify(view.expandedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeOpens_expandedWrapperIsSelected_wrapperNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+ // AND a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.VISIBLE
+ view.onVisibilityAggregated(true)
+
+ // THEN the expandedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the expandedWrapper is shown
+ verify(view.expandedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeCloses_expandedWrapperIsShown_wrapperNotified() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+ // AND a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.GONE
+ view.onVisibilityAggregated(false)
+
+ // THEN the expandedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the expandedWrapper is NOT shown
+ verify(view.expandedWrapper, times(1)).onContentShown(false)
+ }
+
+ @Test
+ fun expandCollapsedNotification_expandedWrapperShown() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+ // AND a collapsed content is created
+ val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
+
+ // WHEN we collapse the notification
+ whenever(row.intrinsicHeight).thenReturn(expandedHeight)
+ view.contentHeight = expandedHeight
+
+ // THEN the wrappers are updated
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ verify(view.contractedWrapper, times(1)).onContentShown(false)
+ verify(view.contractedWrapper).setVisible(false)
+ verify(view.expandedWrapper, times(1)).onContentShown(true)
+ verify(view.expandedWrapper).setVisible(true)
+ }
+
+ @Test
+ fun collapseExpandedNotification_expandedWrapperShown() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+ // AND a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
+
+ // WHEN we collapse the notification
+ whenever(row.intrinsicHeight).thenReturn(contractedHeight)
+ view.contentHeight = contractedHeight
+
+ // THEN the wrappers are updated
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ verify(view.expandedWrapper, times(1)).onContentShown(false)
+ verify(view.expandedWrapper).setVisible(false)
+ verify(view.contractedWrapper, times(1)).onContentShown(true)
+ verify(view.contractedWrapper).setVisible(true)
+ }
@Test
fun testSetFeedbackIcon() {
// Given: contractedChild, enpandedChild, and headsUpChild being set
- val mockContracted = createMockNotificationHeaderView()
- val mockExpanded = createMockNotificationHeaderView()
- val mockHeadsUp = createMockNotificationHeaderView()
-
- with(view) {
- contractedChild = mockContracted
- expandedChild = mockExpanded
- headsUpChild = mockHeadsUp
- }
+ val view = createContentView(isSystemExpanded = false)
// When: FeedBackIcon is set
- view.setFeedbackIcon(
+ val icon =
FeedbackIcon(
R.drawable.ic_feedback_alerted,
R.string.notification_feedback_indicator_alerted
)
- )
+ view.setFeedbackIcon(icon)
- // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible
- verify(mockContracted).visibility = View.VISIBLE
- verify(mockExpanded).visibility = View.VISIBLE
- verify(mockHeadsUp).visibility = View.VISIBLE
+ // Then: contractedChild, enpandedChild, and headsUpChild is updated with the feedbackIcon
+ verify(view.contractedWrapper).setFeedbackIcon(icon)
+ verify(view.expandedWrapper).setFeedbackIcon(icon)
+ verify(view.headsUpWrapper).setFeedbackIcon(icon)
}
- private fun createMockNotificationHeaderView() =
- mock<NotificationHeaderView>().apply {
- whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this)
- whenever(this.context).thenReturn(mContext)
- }
-
@Test
fun testExpandButtonFocusIsCalled() {
val mockContractedEB = mock<NotificationExpandButton>()
- val mockContracted = createMockNotificationHeaderView(mockContractedEB)
+ val mockContracted = createMockNotificationHeaderView(contractedHeight, mockContractedEB)
val mockExpandedEB = mock<NotificationExpandButton>()
- val mockExpanded = createMockNotificationHeaderView(mockExpandedEB)
+ val mockExpanded = createMockNotificationHeaderView(expandedHeight, mockExpandedEB)
val mockHeadsUpEB = mock<NotificationExpandButton>()
- val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB)
+ val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB)
- // Set up all 3 child forms
- view.contractedChild = mockContracted
- view.expandedChild = mockExpanded
- view.headsUpChild = mockHeadsUp
+ val view =
+ createContentView(
+ isSystemExpanded = false,
+ )
+
+ // Update all 3 child forms
+ view.apply {
+ contractedChild = mockContracted
+ expandedChild = mockExpanded
+ headsUpChild = mockHeadsUp
+
+ expandedWrapper = spy(expandedWrapper)
+ }
// This is required to call requestAccessibilityFocus()
view.setFocusOnVisibilityChange()
@@ -143,35 +306,41 @@ class NotificationContentViewTest : SysuiTestCase() {
// The following will initialize the view and switch from not visible to expanded.
// (heads-up is actually an alternate form of contracted, hence this enters expanded state)
view.setHeadsUp(true)
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
verify(mockContractedEB, never()).requestAccessibilityFocus()
verify(mockExpandedEB).requestAccessibilityFocus()
verify(mockHeadsUpEB, never()).requestAccessibilityFocus()
}
- private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) =
- mock<NotificationHeaderView>().apply {
- whenever(this.animate()).thenReturn(mock())
- whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
- whenever(this.context).thenReturn(mContext)
- }
+ private fun createMockNotificationHeaderView(
+ height: Int,
+ mockExpandedEB: NotificationExpandButton
+ ) =
+ spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height })
+ .apply {
+ whenever(this.animate()).thenReturn(mock())
+ whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+ }
@Test
fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
- val mockContracted = mock<NotificationHeaderView>()
+ val mockContracted = spy(createViewWithHeight(contractedHeight))
val mockExpandedActions = mock<NotificationActionListLayout>()
- val mockExpanded = mock<NotificationHeaderView>()
+ val mockExpanded = spy(createViewWithHeight(expandedHeight))
whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
val mockHeadsUpActions = mock<NotificationActionListLayout>()
- val mockHeadsUp = mock<NotificationHeaderView>()
+ val mockHeadsUp = spy(createViewWithHeight(contractedHeight))
whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
- with(view) {
- contractedChild = mockContracted
- expandedChild = mockExpanded
- headsUpChild = mockHeadsUp
- }
+ val view =
+ createContentView(
+ isSystemExpanded = false,
+ contractedView = mockContracted,
+ expandedView = mockExpanded,
+ headsUpView = mockHeadsUp
+ )
view.setRemoteInputVisible(true)
@@ -184,21 +353,23 @@ class NotificationContentViewTest : SysuiTestCase() {
@Test
fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
- val mockContracted = mock<NotificationHeaderView>()
+ val mockContracted = spy(createViewWithHeight(contractedHeight))
val mockExpandedActions = mock<NotificationActionListLayout>()
- val mockExpanded = mock<NotificationHeaderView>()
+ val mockExpanded = spy(createViewWithHeight(expandedHeight))
whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
val mockHeadsUpActions = mock<NotificationActionListLayout>()
- val mockHeadsUp = mock<NotificationHeaderView>()
+ val mockHeadsUp = spy(createViewWithHeight(contractedHeight))
whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
- with(view) {
- contractedChild = mockContracted
- expandedChild = mockExpanded
- headsUpChild = mockHeadsUp
- }
+ val view =
+ createContentView(
+ isSystemExpanded = false,
+ contractedView = mockContracted,
+ expandedView = mockExpanded,
+ headsUpView = mockHeadsUp
+ )
view.setRemoteInputVisible(false)
@@ -212,7 +383,7 @@ class NotificationContentViewTest : SysuiTestCase() {
fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
// Bubble button should not be shown for the given NotificationEntry
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -223,7 +394,9 @@ class NotificationContentViewTest : SysuiTestCase() {
)
)
.thenReturn(actionListMarginTarget)
- view.setContainingNotification(mockContainingNotification)
+ val view = createContentView(isSystemExpanded = false)
+
+ view.setContainingNotification(mockContainingNotification) // maybe not needed
// When: call NotificationContentView.setExpandedChild() to set the expandedChild
view.expandedChild = mockExpandedChild
@@ -237,7 +410,7 @@ class NotificationContentViewTest : SysuiTestCase() {
fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
// Bubble button should be shown for the given NotificationEntry
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -248,10 +421,12 @@ class NotificationContentViewTest : SysuiTestCase() {
)
)
.thenReturn(actionListMarginTarget)
+ val view = createContentView(isSystemExpanded = false)
+
view.setContainingNotification(mockContainingNotification)
// Given: controller says bubbles are enabled for the user
- view.setBubblesEnabledForUser(true);
+ view.setBubblesEnabledForUser(true)
// When: call NotificationContentView.setExpandedChild() to set the expandedChild
view.expandedChild = mockExpandedChild
@@ -263,7 +438,7 @@ class NotificationContentViewTest : SysuiTestCase() {
@Test
fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -274,13 +449,15 @@ class NotificationContentViewTest : SysuiTestCase() {
)
)
.thenReturn(actionListMarginTarget)
+ val view = createContentView(isSystemExpanded = false)
+
view.setContainingNotification(mockContainingNotification)
view.expandedChild = mockExpandedChild
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
// When: call NotificationContentView.onNotificationUpdated() to update the
// NotificationEntry, which should not show bubble button
- view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false))
+ view.onNotificationUpdated(createMockNotificationEntry())
// Then: bottom margin of actionListMarginTarget should not change, still be 20
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
@@ -289,7 +466,7 @@ class NotificationContentViewTest : SysuiTestCase() {
@Test
fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -300,19 +477,20 @@ class NotificationContentViewTest : SysuiTestCase() {
)
)
.thenReturn(actionListMarginTarget)
+ val view = createContentView(isSystemExpanded = false, expandedView = mockExpandedChild)
+
view.setContainingNotification(mockContainingNotification)
- view.expandedChild = mockExpandedChild
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
// When: call NotificationContentView.onNotificationUpdated() to update the
// NotificationEntry, which should show bubble button
- view.onNotificationUpdated(createMockNotificationEntry(true))
+ view.onNotificationUpdated(createMockNotificationEntry(/*true*/ ))
// Then: no bubble yet
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
// Given: controller says bubbles are enabled for the user
- view.setBubblesEnabledForUser(true);
+ view.setBubblesEnabledForUser(true)
// Then: bottom margin of actionListMarginTarget should not change, still be 20
assertEquals(0, getMarginBottom(actionListMarginTarget))
@@ -321,81 +499,63 @@ class NotificationContentViewTest : SysuiTestCase() {
@Test
fun onSetAnimationRunning() {
// Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set
- val mockContracted = mock<NotificationViewWrapper>()
- val mockExpanded = mock<NotificationViewWrapper>()
- val mockHeadsUp = mock<NotificationViewWrapper>()
-
- view.setContractedWrapper(mockContracted)
- view.setExpandedWrapper(mockExpanded)
- view.setHeadsUpWrapper(mockHeadsUp)
+ val view = createContentView(isSystemExpanded = false)
// When: we set content animation running.
assertTrue(view.setContentAnimationRunning(true))
// Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
// called on them.
- verify(mockContracted, times(1)).setAnimationsRunning(true)
- verify(mockExpanded, times(1)).setAnimationsRunning(true)
- verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+ verify(view.contractedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.expandedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.headsUpWrapper, times(1)).setAnimationsRunning(true)
// When: we set content animation running true _again_.
assertFalse(view.setContentAnimationRunning(true))
// Then: the children should not have setAnimationRunning called on them again.
// Verify counts number of calls so far on the object, so these still register as 1.
- verify(mockContracted, times(1)).setAnimationsRunning(true)
- verify(mockExpanded, times(1)).setAnimationsRunning(true)
- verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+ verify(view.contractedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.expandedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.headsUpWrapper, times(1)).setAnimationsRunning(true)
}
@Test
fun onSetAnimationStopped() {
// Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
- val mockContracted = mock<NotificationViewWrapper>()
- val mockExpanded = mock<NotificationViewWrapper>()
- val mockHeadsUp = mock<NotificationViewWrapper>()
-
- view.setContractedWrapper(mockContracted)
- view.setExpandedWrapper(mockExpanded)
- view.setHeadsUpWrapper(mockHeadsUp)
+ val view = createContentView(isSystemExpanded = false)
// When: we set content animation running.
assertTrue(view.setContentAnimationRunning(true))
// Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
// called on them.
- verify(mockContracted).setAnimationsRunning(true)
- verify(mockExpanded).setAnimationsRunning(true)
- verify(mockHeadsUp).setAnimationsRunning(true)
+ verify(view.contractedWrapper).setAnimationsRunning(true)
+ verify(view.expandedWrapper).setAnimationsRunning(true)
+ verify(view.headsUpWrapper).setAnimationsRunning(true)
// When: we set content animation running false, the state changes, so the function
// returns true.
assertTrue(view.setContentAnimationRunning(false))
// Then: the children have their animations stopped.
- verify(mockContracted).setAnimationsRunning(false)
- verify(mockExpanded).setAnimationsRunning(false)
- verify(mockHeadsUp).setAnimationsRunning(false)
+ verify(view.contractedWrapper).setAnimationsRunning(false)
+ verify(view.expandedWrapper).setAnimationsRunning(false)
+ verify(view.headsUpWrapper).setAnimationsRunning(false)
}
@Test
fun onSetAnimationInitStopped() {
// Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
- val mockContracted = mock<NotificationViewWrapper>()
- val mockExpanded = mock<NotificationViewWrapper>()
- val mockHeadsUp = mock<NotificationViewWrapper>()
-
- view.setContractedWrapper(mockContracted)
- view.setExpandedWrapper(mockExpanded)
- view.setHeadsUpWrapper(mockHeadsUp)
+ val view = createContentView(isSystemExpanded = false)
// When: we try to stop the animations before they've been started.
assertFalse(view.setContentAnimationRunning(false))
// Then: the children should not have setAnimationRunning called on them again.
- verify(mockContracted, never()).setAnimationsRunning(false)
- verify(mockExpanded, never()).setAnimationsRunning(false)
- verify(mockHeadsUp, never()).setAnimationsRunning(false)
+ verify(view.contractedWrapper, never()).setAnimationsRunning(false)
+ verify(view.expandedWrapper, never()).setAnimationsRunning(false)
+ verify(view.headsUpWrapper, never()).setAnimationsRunning(false)
}
private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
@@ -405,7 +565,7 @@ class NotificationContentViewTest : SysuiTestCase() {
whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {})
}
- private fun createMockNotificationEntry(showButton: Boolean) =
+ private fun createMockNotificationEntry() =
mock<NotificationEntry>().apply {
whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this))
.thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON)
@@ -426,10 +586,9 @@ class NotificationContentViewTest : SysuiTestCase() {
}
private fun createMockExpandedChild(notificationEntry: NotificationEntry) =
- mock<ExpandableNotificationRow>().apply {
+ spy(createViewWithHeight(expandedHeight)).apply {
whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock())
whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock())
- whenever(this.entry).thenReturn(notificationEntry)
whenever(this.context).thenReturn(mContext)
val resourcesMock: Resources = mock()
@@ -437,6 +596,56 @@ class NotificationContentViewTest : SysuiTestCase() {
whenever(this.resources).thenReturn(resourcesMock)
}
+ private fun createContentView(
+ isSystemExpanded: Boolean,
+ contractedView: View = createViewWithHeight(contractedHeight),
+ expandedView: View = createViewWithHeight(expandedHeight),
+ headsUpView: View = createViewWithHeight(contractedHeight),
+ row: ExpandableNotificationRow = this.row
+ ): NotificationContentView {
+ val height = if (isSystemExpanded) expandedHeight else contractedHeight
+ doReturn(height).whenever(row).intrinsicHeight
+
+ return spy(NotificationContentView(mContext, /* attrs= */ null))
+ .apply {
+ initialize(mPeopleNotificationIdentifier, mock(), mock(), mock(), mock())
+ setContainingNotification(row)
+ setHeights(
+ /* smallHeight= */ contractedHeight,
+ /* headsUpMaxHeight= */ contractedHeight,
+ /* maxHeight= */ expandedHeight
+ )
+ contractedChild = contractedView
+ expandedChild = expandedView
+ headsUpChild = headsUpView
+ contractedWrapper = spy(contractedWrapper)
+ expandedWrapper = spy(expandedWrapper)
+ headsUpWrapper = spy(headsUpWrapper)
+
+ if (isSystemExpanded) {
+ contentHeight = expandedHeight
+ }
+ }
+ .also { contentView ->
+ fakeParent.addView(contentView)
+ contentView.mockRequestLayout()
+ }
+ }
+
+ private fun createViewWithHeight(height: Int) =
+ View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+
private fun getMarginBottom(layout: LinearLayout): Int =
(layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
+
+ private fun px(@DimenRes id: Int): Int = testableResources.resources.getDimensionPixelSize(id)
+}
+
+private fun NotificationContentView.mockRequestLayout() {
+ measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
+ layout(0, 0, measuredWidth, measuredHeight)
+}
+
+private fun NotificationContentView.clearInvocations() {
+ Mockito.clearInvocations(contractedWrapper, expandedWrapper, headsUpWrapper)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 033c96ae84b0..4af7864e6fac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -786,6 +786,25 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
}
@Test
+ public void testSetOwnScrollY_clearAllInProgress_scrollYDoesNotChange() {
+ // Given: clear all is in progress, scrollY is 0
+ mAmbientState.setScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ mAmbientState.setClearAllInProgress(true);
+
+ // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+ mStackScroller.setOwnScrollY(1);
+
+ // Then: scrollY should not change, it should still be 0
+ assertEquals(0, mAmbientState.getScrollY());
+
+ // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests
+ mAmbientState.setClearAllInProgress(false);
+ mStackScroller.setOwnScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ }
+
+ @Test
public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() {
// Given: mAmbientState.mIsClosing is set to be true
// mIsExpanded is set to be false
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 0a7dc4e05633..978fafef7fdc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -191,13 +191,10 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
testScope.runTest {
val isOnLockscreen by collectLastValue(underTest.isOnLockscreen)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- value = 1f,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope,
)
assertThat(isOnLockscreen).isFalse()
@@ -212,19 +209,17 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
)
assertThat(isOnLockscreen).isTrue()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.LOCKSCREEN,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this,
)
assertThat(isOnLockscreen).isTrue()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.PRIMARY_BOUNCER,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ testScope,
)
assertThat(isOnLockscreen).isTrue()
}
@@ -237,11 +232,10 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
// First on AOD
shadeRepository.setLockscreenShadeExpansion(0f)
shadeRepository.setQsExpansion(0f)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.OCCLUDED,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ testScope,
)
assertThat(isOnLockscreenWithoutShade).isFalse()
@@ -271,12 +265,13 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
testScope.runTest {
val position by collectLastValue(underTest.position)
- // Start on lockscreen
- showLockscreen()
-
// When not in split shade
overrideResource(R.bool.config_use_split_notification_shade, false)
configurationRepository.onAnyConfigurationChange()
+ runCurrent()
+
+ // Start on lockscreen
+ showLockscreen()
keyguardInteractor.sharedNotificationContainerPosition.value =
SharedNotificationContainerPosition(top = 1f, bottom = 2f)
@@ -290,12 +285,13 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
testScope.runTest {
val position by collectLastValue(underTest.position)
- // Start on lockscreen
- showLockscreen()
-
// When in split shade
overrideResource(R.bool.config_use_split_notification_shade, true)
configurationRepository.onAnyConfigurationChange()
+ runCurrent()
+
+ // Start on lockscreen
+ showLockscreen()
keyguardInteractor.sharedNotificationContainerPosition.value =
SharedNotificationContainerPosition(top = 1f, bottom = 2f)
@@ -318,7 +314,26 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
sharedNotificationContainerInteractor.setTopPosition(10f)
assertThat(position)
- .isEqualTo(SharedNotificationContainerPosition(top = 10f, bottom = 0f))
+ .isEqualTo(
+ SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = true)
+ )
+ }
+
+ @Test
+ fun positionOnQS() =
+ testScope.runTest {
+ val position by collectLastValue(underTest.position)
+
+ // Start on lockscreen with shade expanded
+ showLockscreenWithQSExpanded()
+
+ // When not in split shade
+ sharedNotificationContainerInteractor.setTopPosition(10f)
+
+ assertThat(position)
+ .isEqualTo(
+ SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = false)
+ )
}
@Test
@@ -372,22 +387,32 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() {
assertThat(maxNotifications).isEqualTo(-1)
}
- private suspend fun showLockscreen() {
+ private suspend fun TestScope.showLockscreen() {
shadeRepository.setLockscreenShadeExpansion(0f)
shadeRepository.setQsExpansion(0f)
keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.LOCKSCREEN,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ this,
)
}
- private suspend fun showLockscreenWithShadeExpanded() {
+ private suspend fun TestScope.showLockscreenWithShadeExpanded() {
shadeRepository.setLockscreenShadeExpansion(1f)
shadeRepository.setQsExpansion(0f)
keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ this,
+ )
+ }
+
+ private suspend fun showLockscreenWithQSExpanded() {
+ shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeRepository.setQsExpansion(1f)
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
to = KeyguardState.LOCKSCREEN,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 842d548c8358..688f739f61f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -99,13 +99,10 @@ class CollapsedStatusBarViewModelImplTest : SysuiTestCase() {
testScope.runTest {
val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- KeyguardState.LOCKSCREEN,
- KeyguardState.OCCLUDED,
- value = 0f,
- TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ this.testScheduler,
)
assertThat(underTest.isTransitioningFromLockscreenToOccluded.value).isFalse()
@@ -312,7 +309,10 @@ class CollapsedStatusBarViewModelImplTest : SysuiTestCase() {
KeyguardState.DREAMING,
value = 1.0f,
TransitionState.FINISHED,
- )
+ ),
+ // We're intentionally not sending STARTED to validate that FINISHED steps are
+ // ignored.
+ validateStep = false,
)
assertThat(emissions).isEmpty()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 71e2bc1339a6..b90ad8cd8745 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.data.repository
import android.annotation.FloatRange
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -26,9 +27,13 @@ import dagger.Binds
import dagger.Module
import java.util.UUID
import javax.inject.Inject
+import junit.framework.Assert.fail
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
/** Fake implementation of [KeyguardTransitionRepository] */
@SysUISingleton
@@ -38,7 +43,111 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio
MutableSharedFlow<TransitionStep>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val transitions: SharedFlow<TransitionStep> = _transitions
- suspend fun sendTransitionStep(step: TransitionStep) {
+ init {
+ _transitions.tryEmit(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.OFF,
+ to = KeyguardState.LOCKSCREEN,
+ )
+ )
+
+ _transitions.tryEmit(
+ TransitionStep(
+ transitionState = TransitionState.FINISHED,
+ from = KeyguardState.OFF,
+ to = KeyguardState.LOCKSCREEN,
+ )
+ )
+ }
+
+ /**
+ * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
+ * [runCurrent] after each step.
+ */
+ suspend fun sendTransitionSteps(
+ from: KeyguardState,
+ to: KeyguardState,
+ testScope: TestScope,
+ ) {
+ sendTransitionSteps(from, to, testScope.testScheduler)
+ }
+
+ /**
+ * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
+ * [runCurrent] after each step.
+ */
+ suspend fun sendTransitionSteps(
+ from: KeyguardState,
+ to: KeyguardState,
+ testScheduler: TestCoroutineScheduler,
+ ) {
+ sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = from,
+ to = to,
+ value = 0f,
+ )
+ )
+ testScheduler.runCurrent()
+
+ sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.RUNNING,
+ from = from,
+ to = to,
+ value = 0.5f
+ )
+ )
+ testScheduler.runCurrent()
+
+ sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.FINISHED,
+ from = from,
+ to = to,
+ value = 1f,
+ )
+ )
+ testScheduler.runCurrent()
+ }
+
+ /**
+ * Directly emits the provided TransitionStep, which can be useful in tests for testing behavior
+ * during specific phases of a transition (such as asserting values while a transition has
+ * STARTED but not FINISHED).
+ *
+ * WARNING: You can get the transition repository into undefined states using this method - for
+ * example, you could send a FINISHED step to LOCKSCREEN having never sent a STARTED step. This
+ * can get flows that combine startedStep/finishedStep into a bad state.
+ *
+ * If you are just trying to get the transition repository FINISHED in a certain state, use
+ * [sendTransitionSteps] - this will send STARTED, RUNNING, and FINISHED steps for you which
+ * ensures that [KeyguardTransitionInteractor] flows will be in the correct state.
+ *
+ * If you're testing something involving transitions themselves and are sure you want to send
+ * only a FINISHED step, override [validateStep].
+ */
+ suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) {
+ _transitions.replayCache.getOrNull(0)?.let { lastStep ->
+ if (
+ validateStep &&
+ step.transitionState == TransitionState.FINISHED &&
+ !(lastStep.transitionState == TransitionState.STARTED ||
+ lastStep.transitionState == TransitionState.RUNNING)
+ ) {
+ fail(
+ "Attempted to send a FINISHED TransitionStep without a prior " +
+ "STARTED/RUNNING step. This leaves the FakeKeyguardTransitionRepository " +
+ "in an undefined state and should not be done. Pass " +
+ "allowInvalidStep=true to sendTransitionStep if you are trying to test " +
+ "this specific and" +
+ "incorrect state."
+ )
+ }
+ }
+
_transitions.emit(step)
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 70c449fe147c..a0363833914a 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -326,7 +326,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
mSensorController = new SensorController(this, mDeviceId,
mParams.getVirtualSensorCallback(), mParams.getVirtualSensorConfigs());
mCameraAccessController = cameraAccessController;
- mCameraAccessController.startObservingIfNeeded();
+ if (mCameraAccessController != null) {
+ mCameraAccessController.startObservingIfNeeded();
+ }
if (!Flags.streamPermissions()) {
mPermissionDialogComponent = getPermissionDialogComponent();
} else {
@@ -566,7 +568,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
mAppToken.unlinkToDeath(this, 0);
- mCameraAccessController.stopObservingIfNeeded();
+ if (mCameraAccessController != null) {
+ mCameraAccessController.stopObservingIfNeeded();
+ }
mInputController.close();
mSensorController.close();
@@ -586,7 +590,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@Override
@RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA)
public void onRunningAppsChanged(ArraySet<Integer> runningUids) {
- mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+ if (mCameraAccessController != null) {
+ mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+ }
mRunningAppsChangedCallback.accept(runningUids);
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 3031a840f4b1..959f69ea483f 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -187,6 +187,9 @@ public class VirtualDeviceManagerService extends SystemService {
}
CameraAccessController getCameraAccessController(UserHandle userHandle) {
+ if (Flags.streamCamera()) {
+ return null;
+ }
int userId = userHandle.getIdentifier();
synchronized (mVirtualDeviceManagerLock) {
for (int i = 0; i < mVirtualDevices.size(); i++) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 166a68a7b22e..7dbf61bab3a9 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -159,7 +159,7 @@ java_library_static {
"android.hardware.boot-V1.2-java", // HIDL
"android.hardware.boot-V1-java", // AIDL
"android.hardware.broadcastradio-V2.0-java", // HIDL
- "android.hardware.broadcastradio-V1-java", // AIDL
+ "android.hardware.broadcastradio-V2-java", // AIDL
"android.hardware.health-V1.0-java", // HIDL
"android.hardware.health-V2.0-java", // HIDL
"android.hardware.health-V2.1-java", // HIDL
@@ -192,12 +192,12 @@ java_library_static {
"com.android.sysprop.watchdog",
"ImmutabilityAnnotation",
"securebox",
- "android.content.pm.flags-aconfig-java",
"apache-commons-math",
"backstage_power_flags_lib",
"notification_flags_lib",
"biometrics_flags_lib",
"am_flags_lib",
+ "com_android_wm_shell_flags_lib",
],
javac_shard_size: 50,
javacflags: [
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 9e48b0a2385b..f9fc4d4f27fa 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -191,6 +191,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
.replaceWith("?");
private static final int MAX_LOW_POWER_STATS_SIZE = 32768;
private static final int POWER_STATS_QUERY_TIMEOUT_MILLIS = 2000;
+ private static final String DEVICE_CONFIG_NAMESPACE = "backstage_power";
private static final String MIN_CONSUMED_POWER_THRESHOLD_KEY = "min_consumed_power_threshold";
private static final String EMPTY = "Empty";
@@ -906,7 +907,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET:
@SuppressLint("MissingPermission")
final double minConsumedPowerThreshold =
- DeviceConfig.getFloat(DeviceConfig.NAMESPACE_BATTERY_STATS,
+ DeviceConfig.getFloat(DEVICE_CONFIG_NAMESPACE,
MIN_CONSUMED_POWER_THRESHOLD_KEY, 0);
final BatteryUsageStatsQuery querySinceReset =
new BatteryUsageStatsQuery.Builder()
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
index 6bed42b54bd9..5b77c5214dd9 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
@@ -23,6 +23,7 @@ import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.hardware.broadcastradio.AmFmRegionConfig;
import android.hardware.broadcastradio.Announcement;
+import android.hardware.broadcastradio.ConfigFlag;
import android.hardware.broadcastradio.DabTableEntry;
import android.hardware.broadcastradio.IdentifierType;
import android.hardware.broadcastradio.Metadata;
@@ -32,6 +33,7 @@ import android.hardware.broadcastradio.ProgramInfo;
import android.hardware.broadcastradio.Properties;
import android.hardware.broadcastradio.Result;
import android.hardware.broadcastradio.VendorKeyValue;
+import android.hardware.radio.Flags;
import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
@@ -45,6 +47,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.utils.Slogf;
import java.util.ArrayList;
@@ -65,13 +68,22 @@ final class ConversionUtils {
/**
* With RADIO_U_VERSION_REQUIRED enabled, 44-bit DAB identifier
- * {@link IdentifierType#DAB_SID_EXT} from broadcast radio HAL can be passed as
- * {@link ProgramSelector#IDENTIFIER_TYPE_DAB_DMB_SID_EXT} to {@link RadioTuner}.
+ * {@code IdentifierType#DAB_SID_EXT} from broadcast radio HAL can be passed as
+ * {@code ProgramSelector#IDENTIFIER_TYPE_DAB_DMB_SID_EXT} to {@code RadioTuner}.
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final long RADIO_U_VERSION_REQUIRED = 261770108L;
+ /**
+ * With RADIO_V_VERSION_REQUIRED enabled, identifier types, config flags and metadata added
+ * in V for HD radio can be passed to {@code RadioTuner} by
+ * {@code android.hardware.radio.ITunerCallback}
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public static final long RADIO_V_VERSION_REQUIRED = 302589903L;
+
private ConversionUtils() {
throw new UnsupportedOperationException("ConversionUtils class is noninstantiable");
}
@@ -81,6 +93,11 @@ final class ConversionUtils {
return CompatChanges.isChangeEnabled(RADIO_U_VERSION_REQUIRED, uid);
}
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ static boolean isAtLeastV(int uid) {
+ return CompatChanges.isChangeEnabled(RADIO_V_VERSION_REQUIRED, uid);
+ }
+
static RuntimeException throwOnError(RuntimeException halException, String action) {
if (!(halException instanceof ServiceSpecificException)) {
return new ParcelableException(new RuntimeException(
@@ -181,6 +198,7 @@ final class ConversionUtils {
// TODO(b/69958423): verify AM/FM with frequency range
return ProgramSelector.PROGRAM_TYPE_FM;
case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
+ case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME:
// TODO(b/69958423): verify AM/FM with frequency range
return ProgramSelector.PROGRAM_TYPE_FM_HD;
case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
@@ -195,6 +213,12 @@ final class ConversionUtils {
case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
return ProgramSelector.PROGRAM_TYPE_SXM;
+ default:
+ if (Flags.hdRadioImproved()) {
+ if (idType == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION) {
+ return ProgramSelector.PROGRAM_TYPE_FM_HD;
+ }
+ }
}
if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
&& idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
@@ -322,9 +346,16 @@ final class ConversionUtils {
static ProgramIdentifier identifierToHalProgramIdentifier(ProgramSelector.Identifier id) {
ProgramIdentifier hwId = new ProgramIdentifier();
- hwId.type = id.getType();
if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) {
hwId.type = IdentifierType.DAB_SID_EXT;
+ } else if (Flags.hdRadioImproved()) {
+ if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION) {
+ hwId.type = IdentifierType.HD_STATION_LOCATION;
+ } else {
+ hwId.type = id.getType();
+ }
+ } else {
+ hwId.type = id.getType();
}
long value = id.getValue();
if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT) {
@@ -344,6 +375,12 @@ final class ConversionUtils {
int idType;
if (id.type == IdentifierType.DAB_SID_EXT) {
idType = ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ } else if (id.type == IdentifierType.HD_STATION_LOCATION) {
+ if (Flags.hdRadioImproved()) {
+ idType = ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION;
+ } else {
+ return null;
+ }
} else {
idType = id.type;
}
@@ -375,7 +412,12 @@ final class ConversionUtils {
ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
ArrayList<ProgramIdentifier> secondaryIdList = new ArrayList<>(secondaryIds.length);
for (int i = 0; i < secondaryIds.length; i++) {
- secondaryIdList.add(identifierToHalProgramIdentifier(secondaryIds[i]));
+ ProgramIdentifier hwId = identifierToHalProgramIdentifier(secondaryIds[i]);
+ if (hwId.type != IdentifierType.INVALID) {
+ secondaryIdList.add(hwId);
+ } else {
+ Slogf.w(TAG, "Invalid secondary id: %s", secondaryIds[i]);
+ }
}
hwSel.secondaryIds = secondaryIdList.toArray(ProgramIdentifier[]::new);
if (!isValidHalProgramSelector(hwSel)) {
@@ -400,7 +442,12 @@ final class ConversionUtils {
List<ProgramSelector.Identifier> secondaryIdList = new ArrayList<>();
for (int i = 0; i < sel.secondaryIds.length; i++) {
if (sel.secondaryIds[i] != null) {
- secondaryIdList.add(identifierFromHalProgramIdentifier(sel.secondaryIds[i]));
+ ProgramSelector.Identifier id = identifierFromHalProgramIdentifier(
+ sel.secondaryIds[i]);
+ if (id == null) {
+ Slogf.e(TAG, "invalid secondary id: %s", sel.secondaryIds[i]);
+ }
+ secondaryIdList.add(id);
}
}
@@ -411,11 +458,13 @@ final class ConversionUtils {
/* vendorIds= */ null);
}
- private static RadioMetadata radioMetadataFromHalMetadata(Metadata[] meta) {
+ @VisibleForTesting
+ static RadioMetadata radioMetadataFromHalMetadata(Metadata[] meta) {
RadioMetadata.Builder builder = new RadioMetadata.Builder();
for (int i = 0; i < meta.length; i++) {
- switch (meta[i].getTag()) {
+ int tag = meta[i].getTag();
+ switch (tag) {
case Metadata.rdsPs:
builder.putString(RadioMetadata.METADATA_KEY_RDS_PS, meta[i].getRdsPs());
break;
@@ -472,10 +521,52 @@ final class ConversionUtils {
meta[i].getDabComponentNameShort());
break;
default:
- Slogf.w(TAG, "Ignored unknown metadata entry: %s", meta[i]);
+ if (Flags.hdRadioImproved()) {
+ switch (tag) {
+ case Metadata.genre:
+ builder.putString(RadioMetadata.METADATA_KEY_GENRE,
+ meta[i].getGenre());
+ break;
+ case Metadata.commentShortDescription:
+ builder.putString(
+ RadioMetadata.METADATA_KEY_COMMENT_SHORT_DESCRIPTION,
+ meta[i].getCommentShortDescription());
+ break;
+ case Metadata.commentActualText:
+ builder.putString(RadioMetadata.METADATA_KEY_COMMENT_ACTUAL_TEXT,
+ meta[i].getCommentActualText());
+ break;
+ case Metadata.commercial:
+ builder.putString(RadioMetadata.METADATA_KEY_COMMERCIAL,
+ meta[i].getCommercial());
+ break;
+ case Metadata.ufids:
+ builder.putStringArray(RadioMetadata.METADATA_KEY_UFIDS,
+ meta[i].getUfids());
+ break;
+ case Metadata.hdStationNameShort:
+ builder.putString(RadioMetadata.METADATA_KEY_HD_STATION_NAME_SHORT,
+ meta[i].getHdStationNameShort());
+ break;
+ case Metadata.hdStationNameLong:
+ builder.putString(RadioMetadata.METADATA_KEY_HD_STATION_NAME_LONG,
+ meta[i].getHdStationNameLong());
+ break;
+ case Metadata.hdSubChannelsAvailable:
+ builder.putInt(RadioMetadata.METADATA_KEY_HD_SUBCHANNELS_AVAILABLE,
+ meta[i].getHdSubChannelsAvailable());
+ break;
+ default:
+ Slogf.w(TAG, "Ignored unknown metadata entry: %s with HD radio flag"
+ + " enabled", meta[i]);
+ break;
+ }
+ } else {
+ Slogf.w(TAG, "Ignored unknown metadata entry: %s with HD radio flag "
+ + "disabled", meta[i]);
+ }
break;
}
-
}
return builder.build();
@@ -547,7 +638,13 @@ final class ConversionUtils {
}
Iterator<ProgramSelector.Identifier> idIterator = filter.getIdentifiers().iterator();
while (idIterator.hasNext()) {
- identifiersList.add(identifierToHalProgramIdentifier(idIterator.next()));
+ ProgramSelector.Identifier id = idIterator.next();
+ ProgramIdentifier hwId = identifierToHalProgramIdentifier(id);
+ if (hwId.type != IdentifierType.INVALID) {
+ identifiersList.add(hwId);
+ } else {
+ Slogf.w(TAG, "Invalid identifiers: %s", id);
+ }
}
hwFilter.identifierTypes = identifierTypeList.toArray();
@@ -558,20 +655,26 @@ final class ConversionUtils {
return hwFilter;
}
- private static boolean isNewIdentifierInU(ProgramSelector.Identifier id) {
- return id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ private static boolean identifierMeetsSdkVersionRequirement(ProgramSelector.Identifier id,
+ int uid) {
+ if (Flags.hdRadioImproved() && !isAtLeastV(uid)) {
+ if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION) {
+ return false;
+ }
+ }
+ if (!isAtLeastU(uid)) {
+ return id.getType() != ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ }
+ return true;
}
static boolean programSelectorMeetsSdkVersionRequirement(ProgramSelector sel, int uid) {
- if (isAtLeastU(uid)) {
- return true;
- }
- if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) {
+ if (!identifierMeetsSdkVersionRequirement(sel.getPrimaryId(), uid)) {
return false;
}
ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
for (int i = 0; i < secondaryIds.length; i++) {
- if (isNewIdentifierInU(secondaryIds[i])) {
+ if (!identifierMeetsSdkVersionRequirement(secondaryIds[i], uid)) {
return false;
}
}
@@ -579,14 +682,11 @@ final class ConversionUtils {
}
static boolean programInfoMeetsSdkVersionRequirement(RadioManager.ProgramInfo info, int uid) {
- if (isAtLeastU(uid)) {
- return true;
- }
if (!programSelectorMeetsSdkVersionRequirement(info.getSelector(), uid)) {
return false;
}
- if (isNewIdentifierInU(info.getLogicallyTunedTo())
- || isNewIdentifierInU(info.getPhysicallyTunedTo())) {
+ if (!identifierMeetsSdkVersionRequirement(info.getLogicallyTunedTo(), uid)
+ || !identifierMeetsSdkVersionRequirement(info.getPhysicallyTunedTo(), uid)) {
return false;
}
if (info.getRelatedContent() == null) {
@@ -594,7 +694,7 @@ final class ConversionUtils {
}
Iterator<ProgramSelector.Identifier> relatedContentIt = info.getRelatedContent().iterator();
while (relatedContentIt.hasNext()) {
- if (isNewIdentifierInU(relatedContentIt.next())) {
+ if (!identifierMeetsSdkVersionRequirement(relatedContentIt.next(), uid)) {
return false;
}
}
@@ -602,9 +702,6 @@ final class ConversionUtils {
}
static ProgramList.Chunk convertChunkToTargetSdkVersion(ProgramList.Chunk chunk, int uid) {
- if (isAtLeastU(uid)) {
- return chunk;
- }
Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
Iterator<RadioManager.ProgramInfo> modifiedIterator = chunk.getModified().iterator();
while (modifiedIterator.hasNext()) {
@@ -617,13 +714,21 @@ final class ConversionUtils {
Iterator<UniqueProgramIdentifier> removedIterator = chunk.getRemoved().iterator();
while (removedIterator.hasNext()) {
UniqueProgramIdentifier id = removedIterator.next();
- if (!isNewIdentifierInU(id.getPrimaryId())) {
+ if (identifierMeetsSdkVersionRequirement(id.getPrimaryId(), uid)) {
removed.add(id);
}
}
return new ProgramList.Chunk(chunk.isPurge(), chunk.isComplete(), modified, removed);
}
+ static boolean configFlagMeetsSdkVersionRequirement(int configFlag, int uid) {
+ if (!Flags.hdRadioImproved() || !isAtLeastV(uid)) {
+ return configFlag != ConfigFlag.FORCE_ANALOG_AM
+ && configFlag != ConfigFlag.FORCE_ANALOG_FM;
+ }
+ return true;
+ }
+
public static android.hardware.radio.Announcement announcementFromHalAnnouncement(
Announcement hwAnnouncement) {
return new android.hardware.radio.Announcement(
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
index 2ae7f9543540..4b3444db38e5 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -167,6 +167,11 @@ final class RadioModule {
fireLater(() -> {
synchronized (mLock) {
fanoutAidlCallbackLocked((cb, uid) -> {
+ if (!ConversionUtils.configFlagMeetsSdkVersionRequirement(flag, uid)) {
+ Slogf.e(TAG, "onConfigFlagUpdated: cannot send program info "
+ + "requiring higher target SDK version");
+ return;
+ }
cb.onConfigFlagUpdated(flag, value);
});
}
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 8e767e74fc9b..8bf903ae0b13 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -18,6 +18,7 @@ package com.android.server.pm;
import static android.Manifest.permission.CONTROL_KEYGUARD;
import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS;
+import static android.content.pm.Flags.sdkLibIndependence;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
@@ -187,7 +188,9 @@ final class DeletePackageHelper {
List<VersionedPackage> libClientPackages =
computer.getPackagesUsingSharedLibrary(libraryInfo,
MATCH_KNOWN_PACKAGES, Process.SYSTEM_UID, currUserId);
- if (!ArrayUtils.isEmpty(libClientPackages)) {
+ boolean allowSdkLibIndependence =
+ (pkg.getSdkLibraryName() != null) && sdkLibIndependence();
+ if (!ArrayUtils.isEmpty(libClientPackages) && !allowSdkLibIndependence) {
Slog.w(TAG, "Not removing package " + pkg.getManifestPackageName()
+ " hosting lib " + libraryInfo.getName() + " version "
+ libraryInfo.getLongVersion() + " used by " + libClientPackages
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index 15a0445b8ad4..1dc9493eddc6 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -29,6 +29,7 @@ import android.os.SystemProperties;
import android.util.Slog;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
+import com.android.wm.shell.Flags;
/**
* The class that defines default launch params for tasks in desktop mode
@@ -40,6 +41,7 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
private static final boolean DEBUG = false;
// Desktop mode feature flags.
+ private static final boolean ENABLE_DESKTOP_WINDOWING = Flags.enableDesktopWindowing();
private static final boolean DESKTOP_MODE_PROTO2_SUPPORTED =
SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false);
// Override default freeform task width when desktop mode is enabled. In dips.
@@ -91,7 +93,7 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
// previous windowing mode to be restored even if the desktop mode state has changed.
// Let task launches inherit the windowing mode from the source task if available, which
// should have the desired windowing mode set by WM Shell. See b/286929122.
- if (DESKTOP_MODE_PROTO2_SUPPORTED && source != null && source.getTask() != null) {
+ if (isDesktopModeSupported() && source != null && source.getTask() != null) {
final Task sourceTask = source.getTask();
outParams.mWindowingMode = sourceTask.getWindowingMode();
appendLog("inherit-from-source=" + outParams.mWindowingMode);
@@ -140,6 +142,12 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
/** Whether desktop mode is supported. */
static boolean isDesktopModeSupported() {
+ // Check for aconfig flag first
+ if (ENABLE_DESKTOP_WINDOWING) {
+ return true;
+ }
+ // Fall back to sysprop flag
+ // TODO(b/304778354): remove sysprop once desktop aconfig flag supports dynamic overriding
return DESKTOP_MODE_PROTO2_SUPPORTED;
}
}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index c81105a11cee..4a467dfbcd14 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2054,6 +2054,12 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
Transition.ReadyCondition pipChangesApplied = new Transition.ReadyCondition("movedToPip");
transitionController.waitFor(pipChangesApplied);
mService.deferWindowLayout();
+ boolean localVisibilityDeferred = false;
+ // If the caller is from WindowOrganizerController, it should be already deferred.
+ if (!mTaskSupervisor.isRootVisibilityUpdateDeferred()) {
+ mTaskSupervisor.setDeferRootVisibilityUpdate(true);
+ localVisibilityDeferred = true;
+ }
try {
// This will change the root pinned task's windowing mode to its original mode, ensuring
// we only have one root task that is in pinned mode.
@@ -2225,14 +2231,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
mService.mTaskFragmentOrganizerController.dispatchPendingInfoChangedEvent(
organizedTf);
}
-
- if (taskDisplayArea.getFocusedRootTask() == rootTask) {
- taskDisplayArea.clearPreferredTopFocusableRootTask();
- }
} finally {
mService.continueWindowLayout();
try {
- if (!isPip2ExperimentEnabled()) {
+ if (localVisibilityDeferred) {
+ mTaskSupervisor.setDeferRootVisibilityUpdate(false);
ensureActivitiesVisible(null, 0, false /* preserveWindows */);
}
} finally {
diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java
index 2be2a1a363db..01fa39b2fa9a 100644
--- a/services/core/java/com/android/server/wm/SnapshotController.java
+++ b/services/core/java/com/android/server/wm/SnapshotController.java
@@ -98,9 +98,14 @@ class SnapshotController {
final TaskFragment tf = info.mContainer.asTaskFragment();
final ActivityRecord ar = tf != null ? tf.getTopMostActivity()
: info.mContainer.asActivityRecord();
- final boolean taskVis = ar != null && ar.getTask().isVisibleRequested();
- if (ar != null && !ar.isVisibleRequested() && taskVis) {
- mActivitySnapshotController.recordSnapshot(ar);
+ if (ar != null && !ar.isVisibleRequested() && ar.getTask().isVisibleRequested()) {
+ final WindowState mainWindow = ar.findMainWindow(false);
+ // Only capture activity snapshot if this app has adapted to back predict
+ if (mainWindow != null
+ && mainWindow.getOnBackInvokedCallbackInfo() != null
+ && mainWindow.getOnBackInvokedCallbackInfo().isSystemCallback()) {
+ mActivitySnapshotController.recordSnapshot(ar);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index ae794a89480a..f0a66540061d 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -412,7 +412,8 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
// wasContained} restricts the preferred root task is set only when moving an existing
// root task to top instead of adding a new root task that may be too early (e.g. in the
// middle of launching or reparenting).
- final boolean isTopFocusableTask = moveToTop && child.isTopActivityFocusable();
+ final boolean isTopFocusableTask = moveToTop && child != mRootPinnedTask
+ && child.isTopActivityFocusable();
if (isTopFocusableTask) {
mPreferredTopFocusableRootTask =
child.shouldBeVisible(null /* starting */) ? child : null;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 9594c65ad5fc..b23ffa8b203e 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1051,7 +1051,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
* @return true if we are *guaranteed* to enter-pip. This means we return false if there's
* a chance we won't thus legacy-entry (via pause+userLeaving) will return false.
*/
- private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar) {
+ private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar,
+ @Nullable ActivityRecord resuming) {
if (!mCanPipOnFinish || !ar.isVisible() || ar.getTask() == null || !ar.isState(RESUMED)) {
return false;
}
@@ -1096,8 +1097,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
try {
// If not going auto-pip, the activity should be paused with user-leaving.
mController.mAtm.mTaskSupervisor.mUserLeaving = true;
- ar.getTaskFragment().startPausing(false /* uiSleeping */,
- null /* resuming */, "finishTransition");
+ ar.getTaskFragment().startPausing(false /* uiSleeping */, resuming, "finishTransition");
} finally {
mController.mAtm.mTaskSupervisor.mUserLeaving = false;
}
@@ -1195,7 +1195,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
final boolean isScreenOff = ar.mDisplayContent == null
|| ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF;
if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) {
- final boolean commitVisibility = !checkEnterPipOnFinish(ar);
+ final ActivityRecord resuming = getVisibleTransientLaunch(
+ ar.getTaskDisplayArea());
+ final boolean commitVisibility = !checkEnterPipOnFinish(ar, resuming);
// Avoid commit visibility if entering pip or else we will get a sudden
// "flash" / surface going invisible for a split second.
if (commitVisibility) {
@@ -1414,6 +1416,22 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
mController.mSnapshotController.onTransitionFinish(mType, mTargets);
}
+ @Nullable
+ private ActivityRecord getVisibleTransientLaunch(TaskDisplayArea taskDisplayArea) {
+ if (mTransientLaunches == null) return null;
+ for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
+ final ActivityRecord candidateActivity = mTransientLaunches.keyAt(i);
+ if (candidateActivity.getTaskDisplayArea() != taskDisplayArea) {
+ continue;
+ }
+ if (!candidateActivity.isVisible()) {
+ continue;
+ }
+ return candidateActivity;
+ }
+ return null;
+ }
+
void abort() {
// This calls back into itself via controller.abort, so just early return here.
if (mState == STATE_ABORT) return;
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index bdbfb7ad80df..17367ef4c072 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -125,6 +125,7 @@ import android.app.servertransaction.ActivityConfigurationChangeItem;
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.DestroyActivityItem;
import android.app.servertransaction.PauseActivityItem;
+import android.app.servertransaction.WindowStateResizeItem;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -3341,6 +3342,7 @@ public class ActivityRecordTests extends WindowTestsBase {
// Simulate switching to app2 to make it visible to be IME targets.
spyOn(app2);
spyOn(app2.mClient);
+ spyOn(app2.getProcess());
ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class);
doReturn(true).when(app2).isReadyToDispatchInsetsState();
mDisplayContent.setImeLayeringTarget(app2);
@@ -3351,9 +3353,15 @@ public class ActivityRecordTests extends WindowTestsBase {
// Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets
// to client if the app didn't request IME visible.
assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
- verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
- insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
- anyBoolean());
+
+ if (mWm.mFlags.mWindowStateResizeItemFlag) {
+ verify(app2.getProcess()).scheduleClientTransactionItem(
+ isA(WindowStateResizeItem.class));
+ } else {
+ verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
+ insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
+ anyBoolean());
+ }
assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index baf259461847..1aa34eebbbb3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1439,6 +1439,7 @@ public class TransitionTests extends WindowTestsBase {
activity1.setVisibleRequested(true);
activity1.setVisible(true);
activity2.setVisibleRequested(false);
+ activity1.setState(ActivityRecord.State.RESUMED, "test");
// Using abort to force-finish the sync (since we can't wait for drawing in unit test).
// We didn't call abort on the transition itself, so it will still run onTransactionReady
@@ -1517,6 +1518,8 @@ public class TransitionTests extends WindowTestsBase {
// Make sure activity1 visibility was committed
assertFalse(activity1.isVisible());
assertFalse(activity1.app.hasActivityInVisibleTask());
+ // Make sure the userLeaving is true and the resuming activity is given,
+ verify(task1).startPausing(eq(true), anyBoolean(), eq(activity2), any());
verify(taskSnapshotController, times(1)).recordSnapshot(eq(task1));
assertTrue(enteringAnimReports.contains(activity2));
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index f8608b8fead2..8e90fe7ea975 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -265,8 +265,8 @@ public class SubscriptionManager {
}
}
- private static IntegerPropertyInvalidatedCache<Integer> sGetDefaultSubIdCacheAsUser =
- new IntegerPropertyInvalidatedCache<>(ISub::getDefaultSubIdAsUser,
+ private static VoidPropertyInvalidatedCache<Integer> sGetDefaultSubIdCache =
+ new VoidPropertyInvalidatedCache<>(ISub::getDefaultSubId,
CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
INVALID_SUBSCRIPTION_ID);
@@ -275,8 +275,8 @@ public class SubscriptionManager {
CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
INVALID_SUBSCRIPTION_ID);
- private static IntegerPropertyInvalidatedCache<Integer> sGetDefaultSmsSubIdCacheAsUser =
- new IntegerPropertyInvalidatedCache<>(ISub::getDefaultSmsSubIdAsUser,
+ private static VoidPropertyInvalidatedCache<Integer> sGetDefaultSmsSubIdCache =
+ new VoidPropertyInvalidatedCache<>(ISub::getDefaultSmsSubId,
CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
INVALID_SUBSCRIPTION_ID);
@@ -2309,7 +2309,7 @@ public class SubscriptionManager {
* @return the "system" default subscription id.
*/
public static int getDefaultSubscriptionId() {
- return sGetDefaultSubIdCacheAsUser.query(Process.myUserHandle().getIdentifier());
+ return sGetDefaultSubIdCache.query(null);
}
/**
@@ -2325,7 +2325,7 @@ public class SubscriptionManager {
try {
ISub iSub = TelephonyManager.getSubscriptionService();
if (iSub != null) {
- subId = iSub.getDefaultVoiceSubIdAsUser(Process.myUserHandle().getIdentifier());
+ subId = iSub.getDefaultVoiceSubId();
}
} catch (RemoteException ex) {
// ignore it
@@ -2397,7 +2397,7 @@ public class SubscriptionManager {
* @return the default SMS subscription Id.
*/
public static int getDefaultSmsSubscriptionId() {
- return sGetDefaultSmsSubIdCacheAsUser.query(Process.myUserHandle().getIdentifier());
+ return sGetDefaultSmsSubIdCache.query(null);
}
/**
@@ -3927,10 +3927,10 @@ public class SubscriptionManager {
* @hide
*/
public static void disableCaching() {
- sGetDefaultSubIdCacheAsUser.disableLocal();
+ sGetDefaultSubIdCache.disableLocal();
sGetDefaultDataSubIdCache.disableLocal();
sGetActiveDataSubscriptionIdCache.disableLocal();
- sGetDefaultSmsSubIdCacheAsUser.disableLocal();
+ sGetDefaultSmsSubIdCache.disableLocal();
sGetSlotIndexCache.disableLocal();
sGetSubIdCache.disableLocal();
sGetPhoneIdCache.disableLocal();
@@ -3941,10 +3941,10 @@ public class SubscriptionManager {
*
* @hide */
public static void clearCaches() {
- sGetDefaultSubIdCacheAsUser.clear();
+ sGetDefaultSubIdCache.clear();
sGetDefaultDataSubIdCache.clear();
sGetActiveDataSubscriptionIdCache.clear();
- sGetDefaultSmsSubIdCacheAsUser.clear();
+ sGetDefaultSmsSubIdCache.clear();
sGetSlotIndexCache.clear();
sGetSubIdCache.clear();
sGetPhoneIdCache.clear();
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index d2dbeb7aff74..a5a23e8659d8 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -239,7 +239,6 @@ interface ISub {
int getSubId(int slotIndex);
int getDefaultSubId();
- int getDefaultSubIdAsUser(int userId);
int getPhoneId(int subId);
@@ -252,12 +251,10 @@ interface ISub {
void setDefaultDataSubId(int subId);
int getDefaultVoiceSubId();
- int getDefaultVoiceSubIdAsUser(int userId);
void setDefaultVoiceSubId(int subId);
int getDefaultSmsSubId();
- int getDefaultSmsSubIdAsUser(int userId);
void setDefaultSmsSubId(int subId);
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 977b2768e702..fff8f78a5d01 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -122,7 +122,6 @@ cc_library_host_static {
"link/AutoVersioner.cpp",
"link/ManifestFixer.cpp",
"link/NoDefaultResourceRemover.cpp",
- "link/ProductFilter.cpp",
"link/PrivateAttributeMover.cpp",
"link/ReferenceLinker.cpp",
"link/ResourceExcluder.cpp",
@@ -135,6 +134,7 @@ cc_library_host_static {
"optimize/ResourceFilter.cpp",
"optimize/Obfuscator.cpp",
"optimize/VersionCollapser.cpp",
+ "process/ProductFilter.cpp",
"process/SymbolTable.cpp",
"split/TableSplitter.cpp",
"text/Printer.cpp",
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index b5c290ec8dad..728ba8aa4fdd 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -45,6 +45,7 @@
#include "io/StringStream.h"
#include "io/Util.h"
#include "io/ZipArchive.h"
+#include "process/ProductFilter.h"
#include "trace/TraceBuffer.h"
#include "util/Files.h"
#include "util/Util.h"
@@ -179,6 +180,15 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options,
if (!res_parser.Parse(&xml_parser)) {
return false;
}
+
+ if (options.product_.has_value()) {
+ if (!ProductFilter({*options.product_}, /* remove_default_config_values = */ true)
+ .Consume(context, &table)) {
+ context->GetDiagnostics()->Error(android::DiagMessage(path_data.source)
+ << "failed to filter product");
+ return false;
+ }
+ }
}
if (options.pseudolocalize && translatable_file) {
diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h
index 22890fc53d5c..61c5b60adb76 100644
--- a/tools/aapt2/cmd/Compile.h
+++ b/tools/aapt2/cmd/Compile.h
@@ -44,6 +44,7 @@ struct CompileOptions {
// See comments on aapt::ResourceParserOptions.
bool preserve_visibility_of_styleables = false;
bool verbose = false;
+ std::optional<std::string> product_;
};
/** Parses flags and compiles resources to be used in linking. */
@@ -87,6 +88,10 @@ class CompileCommand : public Command {
"Sets the ratio of resources to generate grammatical gender strings for. The "
"ratio has to be a float number between 0 and 1.",
&options_.pseudo_localize_gender_ratio);
+ AddOptionalFlag("--filter-product",
+ "Leave only resources specific to the given product. All "
+ "other resources (including defaults) are removed.",
+ &options_.product_);
}
int Action(const std::vector<std::string>& args) override;
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index eb4e38c5f35f..159c6fd9ef58 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -66,6 +66,7 @@
#include "optimize/ResourceDeduper.h"
#include "optimize/VersionCollapser.h"
#include "process/IResourceTableConsumer.h"
+#include "process/ProductFilter.h"
#include "process/SymbolTable.h"
#include "split/TableSplitter.h"
#include "trace/TraceBuffer.h"
@@ -2128,7 +2129,7 @@ class Linker {
<< "can't select products when building static library");
}
} else {
- ProductFilter product_filter(options_.products);
+ ProductFilter product_filter(options_.products, /* remove_default_config_values = */ false);
if (!product_filter.Consume(context_, &final_table_)) {
context_->GetDiagnostics()->Error(android::DiagMessage() << "failed stripping products");
return 1;
diff --git a/tools/aapt2/link/Linkers.h b/tools/aapt2/link/Linkers.h
index 44cd276f77a2..18165f7d489f 100644
--- a/tools/aapt2/link/Linkers.h
+++ b/tools/aapt2/link/Linkers.h
@@ -20,12 +20,12 @@
#include <set>
#include <unordered_set>
+#include "Resource.h"
+#include "SdkConstants.h"
#include "android-base/macros.h"
+#include "android-base/result.h"
#include "androidfw/ConfigDescription.h"
#include "androidfw/StringPiece.h"
-
-#include "Resource.h"
-#include "SdkConstants.h"
#include "process/IResourceTableConsumer.h"
#include "xml/XmlDom.h"
@@ -92,28 +92,6 @@ class PrivateAttributeMover : public IResourceTableConsumer {
DISALLOW_COPY_AND_ASSIGN(PrivateAttributeMover);
};
-class ResourceConfigValue;
-
-class ProductFilter : public IResourceTableConsumer {
- public:
- using ResourceConfigValueIter = std::vector<std::unique_ptr<ResourceConfigValue>>::iterator;
-
- explicit ProductFilter(std::unordered_set<std::string> products) : products_(products) {
- }
-
- ResourceConfigValueIter SelectProductToKeep(const ResourceNameRef& name,
- const ResourceConfigValueIter begin,
- const ResourceConfigValueIter end,
- android::IDiagnostics* diag);
-
- bool Consume(IAaptContext* context, ResourceTable* table) override;
-
- private:
- DISALLOW_COPY_AND_ASSIGN(ProductFilter);
-
- std::unordered_set<std::string> products_;
-};
-
// Removes namespace nodes and URI information from the XmlResource.
//
// Once an XmlResource is processed by this consumer, it is no longer able to have its attributes
diff --git a/tools/aapt2/link/ProductFilter.cpp b/tools/aapt2/process/ProductFilter.cpp
index 9544986fda76..0b1c0a6adb51 100644
--- a/tools/aapt2/link/ProductFilter.cpp
+++ b/tools/aapt2/process/ProductFilter.cpp
@@ -14,16 +14,18 @@
* limitations under the License.
*/
-#include "link/Linkers.h"
+#include "process/ProductFilter.h"
+
+#include <algorithm>
#include "ResourceTable.h"
#include "trace/TraceBuffer.h"
namespace aapt {
-ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep(
- const ResourceNameRef& name, const ResourceConfigValueIter begin,
- const ResourceConfigValueIter end, android::IDiagnostics* diag) {
+std::optional<ProductFilter::ResourceConfigValueIter> ProductFilter::SelectProductToKeep(
+ const ResourceNameRef& name, ResourceConfigValueIter begin, ResourceConfigValueIter end,
+ android::IDiagnostics* diag) {
ResourceConfigValueIter default_product_iter = end;
ResourceConfigValueIter selected_product_iter = end;
@@ -36,12 +38,11 @@ ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep(
<< "selection of product '" << config_value->product << "' for resource "
<< name << " is ambiguous");
- ResourceConfigValue* previously_selected_config_value =
- selected_product_iter->get();
+ ResourceConfigValue* previously_selected_config_value = selected_product_iter->get();
diag->Note(android::DiagMessage(previously_selected_config_value->value->GetSource())
<< "product '" << previously_selected_config_value->product
<< "' is also a candidate");
- return end;
+ return std::nullopt;
}
// Select this product.
@@ -54,11 +55,10 @@ ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep(
diag->Error(android::DiagMessage(config_value->value->GetSource())
<< "multiple default products defined for resource " << name);
- ResourceConfigValue* previously_default_config_value =
- default_product_iter->get();
+ ResourceConfigValue* previously_default_config_value = default_product_iter->get();
diag->Note(android::DiagMessage(previously_default_config_value->value->GetSource())
<< "default product also defined here");
- return end;
+ return std::nullopt;
}
// Mark the default.
@@ -66,9 +66,16 @@ ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep(
}
}
+ if (remove_default_config_values_) {
+ // If we are leaving only a specific product, return early here instead of selecting the default
+ // value. Returning end here will cause this value set to be skipped, and will be removed with
+ // ClearEmptyValues method.
+ return selected_product_iter;
+ }
+
if (default_product_iter == end) {
diag->Error(android::DiagMessage() << "no default product defined for resource " << name);
- return end;
+ return std::nullopt;
}
if (selected_product_iter == end) {
@@ -89,20 +96,27 @@ bool ProductFilter::Consume(IAaptContext* context, ResourceTable* table) {
ResourceConfigValueIter start_range_iter = iter;
while (iter != entry->values.end()) {
++iter;
- if (iter == entry->values.end() ||
- (*iter)->config != (*start_range_iter)->config) {
+ if (iter == entry->values.end() || (*iter)->config != (*start_range_iter)->config) {
// End of the array, or we saw a different config,
// so this must be the end of a range of products.
// Select the product to keep from the set of products defined.
ResourceNameRef name(pkg->name, type->named_type, entry->name);
- auto value_to_keep = SelectProductToKeep(
- name, start_range_iter, iter, context->GetDiagnostics());
- if (value_to_keep == iter) {
+ auto value_to_keep =
+ SelectProductToKeep(name, start_range_iter, iter, context->GetDiagnostics());
+ if (!value_to_keep.has_value()) {
// An error occurred, we could not pick a product.
error = true;
- } else {
+ } else if (auto val = value_to_keep.value(); val != iter) {
// We selected a product to keep. Move it to the new array.
- new_values.push_back(std::move(*value_to_keep));
+ if (remove_default_config_values_) {
+ // We are filtering values with the given product. The selected value here will be
+ // a new default value, and all other values will be removed.
+ new_values.push_back(
+ std::make_unique<ResourceConfigValue>((*val)->config, android::StringPiece{}));
+ new_values.back()->value = std::move((*val)->value);
+ } else {
+ new_values.push_back(std::move(*val));
+ }
}
// Start the next range of products.
@@ -115,7 +129,27 @@ bool ProductFilter::Consume(IAaptContext* context, ResourceTable* table) {
}
}
}
+
+ if (remove_default_config_values_) {
+ ClearEmptyValues(table);
+ }
+
return !error;
}
+void ProductFilter::ClearEmptyValues(ResourceTable* table) {
+ // Clear any empty packages/types/entries, as remove_default_config_values_ may remove an entire
+ // value set.
+ CHECK(remove_default_config_values_)
+ << __func__ << " should only be called when remove_default_config_values_ is set";
+
+ for (auto& pkg : table->packages) {
+ for (auto& type : pkg->types) {
+ std::erase_if(type->entries, [](auto& entry) { return entry->values.empty(); });
+ }
+ std::erase_if(pkg->types, [](auto& type) { return type->entries.empty(); });
+ }
+ std::erase_if(table->packages, [](auto& package) { return package->types.empty(); });
+}
+
} // namespace aapt
diff --git a/tools/aapt2/process/ProductFilter.h b/tools/aapt2/process/ProductFilter.h
new file mode 100644
index 000000000000..0ec2f00863fc
--- /dev/null
+++ b/tools/aapt2/process/ProductFilter.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "Resource.h"
+#include "android-base/macros.h"
+#include "androidfw/ConfigDescription.h"
+#include "androidfw/IDiagnostics.h"
+#include "process/IResourceTableConsumer.h"
+
+namespace aapt {
+
+class ResourceConfigValue;
+
+class ProductFilter : public IResourceTableConsumer {
+ public:
+ using ResourceConfigValueIter = std::vector<std::unique_ptr<ResourceConfigValue>>::iterator;
+
+ // Setting remove_default_config_values will remove all values other than
+ // specified product, including default. For example, if the following table
+ //
+ // <string name="foo" product="default">foo_default</string>
+ // <string name="foo" product="tablet">foo_tablet</string>
+ // <string name="bar">bar</string>
+ //
+ // is consumed with tablet, it will result in
+ //
+ // <string name="foo">foo_tablet</string>
+ //
+ // removing foo_default and bar. This option is to generate an RRO package
+ // with given product.
+ explicit ProductFilter(std::unordered_set<std::string> products,
+ bool remove_default_config_values)
+ : products_(std::move(products)),
+ remove_default_config_values_(remove_default_config_values) {
+ }
+
+ bool Consume(IAaptContext* context, ResourceTable* table) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ProductFilter);
+
+ // SelectProductToKeep returns an iterator for the selected value.
+ //
+ // Returns std::nullopt in case of failure (e.g. ambiguous values, missing or duplicated default
+ // values).
+ // Returns `end` if keep_as_default_product is set and no value for the specified product was
+ // found.
+ std::optional<ResourceConfigValueIter> SelectProductToKeep(const ResourceNameRef& name,
+ ResourceConfigValueIter begin,
+ ResourceConfigValueIter end,
+ android::IDiagnostics* diag);
+
+ void ClearEmptyValues(ResourceTable* table);
+
+ std::unordered_set<std::string> products_;
+ bool remove_default_config_values_;
+};
+
+} // namespace aapt
diff --git a/tools/aapt2/link/ProductFilter_test.cpp b/tools/aapt2/process/ProductFilter_test.cpp
index 2cb9afa05cad..27a82dcc3453 100644
--- a/tools/aapt2/link/ProductFilter_test.cpp
+++ b/tools/aapt2/process/ProductFilter_test.cpp
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "link/Linkers.h"
+#include "process/ProductFilter.h"
#include "test/Test.h"
@@ -57,17 +57,15 @@ TEST(ProductFilterTest, SelectTwoProducts) {
.Build(),
context->GetDiagnostics()));
- ProductFilter filter({"tablet"});
+ ProductFilter filter({"tablet"}, /* remove_default_config_values = */ false);
ASSERT_TRUE(filter.Consume(context.get(), &table));
- EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", land, ""));
- EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", land, "tablet"));
- EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", port, ""));
- EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", port, "tablet"));
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, ""));
+ EXPECT_NE(nullptr,
+ test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, "tablet"));
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, ""));
+ EXPECT_NE(nullptr,
+ test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, "tablet"));
}
TEST(ProductFilterTest, SelectDefaultProduct) {
@@ -88,15 +86,15 @@ TEST(ProductFilterTest, SelectDefaultProduct) {
context->GetDiagnostics()));
;
- ProductFilter filter(std::unordered_set<std::string>{});
+ ProductFilter filter(std::unordered_set<std::string>{},
+ /* remove_default_config_values = */ false);
ASSERT_TRUE(filter.Consume(context.get(), &table));
- EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one",
- ConfigDescription::DefaultConfig(), ""));
- EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one",
- ConfigDescription::DefaultConfig(), "tablet"));
+ EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one",
+ ConfigDescription::DefaultConfig(), ""));
+ EXPECT_EQ(nullptr,
+ test::GetValueForConfigAndProduct<Id>(&table, "android:string/one",
+ ConfigDescription::DefaultConfig(), "tablet"));
}
TEST(ProductFilterTest, FailOnAmbiguousProduct) {
@@ -123,7 +121,7 @@ TEST(ProductFilterTest, FailOnAmbiguousProduct) {
.Build(),
context->GetDiagnostics()));
- ProductFilter filter({"tablet", "no-sdcard"});
+ ProductFilter filter({"tablet", "no-sdcard"}, /* remove_default_config_values = */ false);
ASSERT_FALSE(filter.Consume(context.get(), &table));
}
@@ -144,8 +142,67 @@ TEST(ProductFilterTest, FailOnMultipleDefaults) {
.Build(),
context->GetDiagnostics()));
- ProductFilter filter(std::unordered_set<std::string>{});
+ ProductFilter filter(std::unordered_set<std::string>{},
+ /* remove_default_config_values = */ false);
ASSERT_FALSE(filter.Consume(context.get(), &table));
}
+TEST(ProductFilterTest, RemoveDefaultConfigValues) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ const ConfigDescription land = test::ParseConfigOrDie("land");
+ const ConfigDescription port = test::ParseConfigOrDie("port");
+
+ ResourceTable table;
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(),
+ land)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/tablet.xml")).Build(),
+ land, "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/two"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(),
+ land)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(),
+ port)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/tablet.xml")).Build(),
+ port, "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/two"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(),
+ port)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ProductFilter filter({"tablet"}, /* remove_default_config_values = */ true);
+ ASSERT_TRUE(filter.Consume(context.get(), &table));
+
+ EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, ""));
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/two", land, ""));
+ EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, ""));
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/two", port, ""));
+}
+
} // namespace aapt