diff options
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 |