diff options
47 files changed, 1457 insertions, 471 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2ce3609d77e7..746035462329 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2940,6 +2940,13 @@ package android.app.smartspace.uitemplatedata { package android.app.supervision { + @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public class SupervisionAppService extends android.app.Service { + ctor public SupervisionAppService(); + method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent); + method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onDisabled(); + method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onEnabled(); + } + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent(); method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); diff --git a/core/java/android/app/supervision/SupervisionAppService.java b/core/java/android/app/supervision/SupervisionAppService.java index 4530be5c270a..93eb96204444 100644 --- a/core/java/android/app/supervision/SupervisionAppService.java +++ b/core/java/android/app/supervision/SupervisionAppService.java @@ -16,7 +16,11 @@ package android.app.supervision; +import android.annotation.FlaggedApi; +import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.Service; +import android.app.supervision.flags.Flags; import android.content.Intent; import android.os.IBinder; @@ -26,31 +30,43 @@ import android.os.IBinder; * * @hide */ +@SystemApi +@FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE) public class SupervisionAppService extends Service { - private final ISupervisionAppService mBinder = new ISupervisionAppService.Stub() { - @Override - public void onEnabled() { - SupervisionAppService.this.onEnabled(); - } + private final ISupervisionAppService mBinder = + new ISupervisionAppService.Stub() { + @Override + public void onEnabled() { + SupervisionAppService.this.onEnabled(); + } - @Override - public void onDisabled() { - SupervisionAppService.this.onDisabled(); - } - }; + @Override + public void onDisabled() { + SupervisionAppService.this.onDisabled(); + } + }; + @Nullable @Override - public final IBinder onBind(Intent intent) { + public final IBinder onBind(@Nullable Intent intent) { return mBinder.asBinder(); } /** * Called when supervision is enabled. + * + * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE) public void onEnabled() {} /** * Called when supervision is disabled. + * + * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE) public void onDisabled() {} } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index e47adc90fc7a..1a74fe6719e3 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1219,6 +1219,8 @@ 6 - Lock if keyguard enabled or go to sleep (doze) 7 - Dream if possible or go to sleep (doze) 8 - Go to glanceable hub or dream if possible, or sleep if neither is available (doze) + 9 - Go to dream if device is not dreaming, stop dream if device is dreaming, or sleep if + neither is available (doze) --> <integer name="config_shortPressOnPowerBehavior">1</integer> diff --git a/core/tests/FileSystemUtilsTest/OWNERS b/core/tests/FileSystemUtilsTest/OWNERS new file mode 100644 index 000000000000..74eeacfeb973 --- /dev/null +++ b/core/tests/FileSystemUtilsTest/OWNERS @@ -0,0 +1,2 @@ +waghpawan@google.com +kaleshsingh@google.com diff --git a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java index 208d74e49afe..dbfd3e8ccdaa 100644 --- a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java +++ b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java @@ -38,6 +38,8 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test { private static final String PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM = "app_with_4kb_elf_no_override.apk"; + private static final int DEVICE_WAIT_TIMEOUT = 120000; + @Test @AppModeFull public void runPunchedApp_embeddedNativeLibs() throws DeviceNotAvailableException { @@ -98,8 +100,20 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test { @AppModeFull public void runAppWith4KbLib_compatByAlignmentChecks() throws DeviceNotAvailableException, TargetSetupError { + // make sure that device is available for UI test + prepareDevice(); // This test is expected to fail since compat is disabled in manifest runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM, "testPageSizeCompat_compatByAlignmentChecks"); } + + private void prepareDevice() throws DeviceNotAvailableException { + // Verify that device is online before running test and enable root + getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT); + getDevice().enableAdbRoot(); + getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT); + + getDevice().executeShellCommand("input keyevent KEYCODE_WAKEUP"); + getDevice().executeShellCommand("wm dismiss-keyguard"); + } } diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp index 01e8010444c0..64d38b9ef466 100644 --- a/libs/hwui/renderthread/ReliableSurface.cpp +++ b/libs/hwui/renderthread/ReliableSurface.cpp @@ -149,25 +149,9 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) { return AHardwareBuffer_to_ANativeWindowBuffer(mScratchBuffer.get()); } - int width = -1; - int result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_WIDTH, &width); - if (result != OK || width < 0) { - ALOGW("Failed to query window default width: %s (%d) value=%d", strerror(-result), result, - width); - width = 1; - } - - int height = -1; - result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_HEIGHT, &height); - if (result != OK || height < 0) { - ALOGW("Failed to query window default height: %s (%d) value=%d", strerror(-result), result, - height); - height = 1; - } - AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{ - .width = static_cast<uint32_t>(width), - .height = static_cast<uint32_t>(height), + .width = 1, + .height = 1, .layers = 1, .format = mFormat, .usage = mUsage, @@ -176,9 +160,9 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) { }; AHardwareBuffer* newBuffer; - result = AHardwareBuffer_allocate(&desc, &newBuffer); + int result = AHardwareBuffer_allocate(&desc, &newBuffer); - if (result != OK) { + if (result != NO_ERROR) { // Allocate failed, that sucks ALOGW("Failed to allocate scratch buffer, error=%d", result); return nullptr; diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index a67aea466c1c..0cd9c53c830f 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -238,6 +238,7 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, for (uint32_t i = 0; i < queueCount; i++) { queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT; queuePriorityProps[i].pNext = nullptr; + queueProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2; queueProps[i].pNext = &queuePriorityProps[i]; } mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get()); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index a4852d26ffe2..cea91a18ae32 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1856,10 +1856,11 @@ flag { } flag { - name: "shade_header_fonts" + name: "shade_header_font_update" namespace: "systemui" description: "Updates the fonts of the shade header" - bug: "393609724" + bug: "393609960" + is_fixed_read_only: true } flag { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 3ecf302204bc..67af7a54988e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.DozeParameters @@ -45,6 +46,8 @@ import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.wm.shell.appzoomout.AppZoomOut import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.function.Consumer import org.junit.Before import org.junit.Rule import org.junit.Test @@ -65,8 +68,6 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit -import java.util.Optional -import java.util.function.Consumer @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -75,6 +76,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { private val kosmos = testKosmos() private val applicationScope = kosmos.testScope.backgroundScope + private val shadeDisplayRepository = kosmos.fakeShadeDisplaysRepository @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var blurUtils: BlurUtils @Mock private lateinit var biometricUnlockController: BiometricUnlockController @@ -135,7 +137,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { windowRootViewBlurInteractor, appZoomOutOptional, applicationScope, - dumpManager + dumpManager, + { shadeDisplayRepository }, ) notificationShadeDepthController.shadeAnimation = shadeAnimation notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring @@ -355,6 +358,36 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun updateBlurCallback_shadeInExternalDisplay_doesSetZeroZoom() { + notificationShadeDepthController.onPanelExpansionChanged( + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) + notificationShadeDepthController.addListener(listener) + shadeDisplayRepository.setDisplayId(1) // not default display. + + notificationShadeDepthController.updateBlurCallback.doFrame(0) + + verify(wallpaperController).setNotificationShadeZoom(eq(0f)) + verify(listener).onWallpaperZoomOutChanged(eq(0f)) + } + + @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun updateBlurCallback_shadeInDefaultDisplay_doesNotSetZeroZoom() { + notificationShadeDepthController.onPanelExpansionChanged( + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) + notificationShadeDepthController.addListener(listener) + shadeDisplayRepository.setDisplayId(0) // shade is in default display + + notificationShadeDepthController.updateBlurCallback.doFrame(0) + + verify(wallpaperController).setNotificationShadeZoom(floatThat { it != 0f }) + verify(listener).onWallpaperZoomOutChanged(floatThat { it != 0f }) + } + + @Test @DisableFlags(Flags.FLAG_NOTIFICATION_SHADE_BLUR) fun updateBlurCallback_setsOpaque_whenScrim() { scrimVisibilityCaptor.value.accept(ScrimController.OPAQUE) @@ -488,10 +521,10 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } private fun enableSplitShade() { - `when` (shadeModeInteractor.isSplitShade).thenReturn(true) + `when`(shadeModeInteractor.isSplitShade).thenReturn(true) } private fun disableSplitShade() { - `when` (shadeModeInteractor.isSplitShade).thenReturn(false) + `when`(shadeModeInteractor.isSplitShade).thenReturn(false) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index ea7321627322..b8cd5bec2cbe 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -208,4 +208,19 @@ public class PreviewPositionHelper { } mMatrix.postTranslate(translateX, translateY); } + + /** + * A factory that returns a new instance of the {@link PreviewPositionHelper}. + * <p>{@link PreviewPositionHelper} is a stateful helper, and hence when using it in distinct + * scenarios, prefer fetching an object using this factory</p> + * <p>Additionally, helpful for injecting mocks in tests</p> + */ + public static class PreviewPositionHelperFactory { + /** + * Returns a new {@link PreviewPositionHelper} for use in a distinct scenario. + */ + public PreviewPositionHelper create() { + return new PreviewPositionHelper(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 6831c412d1e4..099a7f067482 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -140,6 +140,7 @@ import com.android.systemui.animation.TransitionAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor; import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -364,6 +365,7 @@ public class KeyguardViewMediator implements CoreStartable, private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController; private final Lazy<ShadeController> mShadeController; private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor; + private final Lazy<CommunalSettingsInteractor> mCommunalSettingsInteractor; /* * Records the user id on request to go away, for validation when WM calls back to start the * exit animation. @@ -1567,6 +1569,7 @@ public class KeyguardViewMediator implements CoreStartable, KeyguardInteractor keyguardInteractor, KeyguardTransitionBootInteractor transitionBootInteractor, Lazy<CommunalSceneInteractor> communalSceneInteractor, + Lazy<CommunalSettingsInteractor> communalSettingsInteractor, WindowManagerOcclusionManager wmOcclusionManager) { mContext = context; mUserTracker = userTracker; @@ -1609,6 +1612,7 @@ public class KeyguardViewMediator implements CoreStartable, mKeyguardInteractor = keyguardInteractor; mTransitionBootInteractor = transitionBootInteractor; mCommunalSceneInteractor = communalSceneInteractor; + mCommunalSettingsInteractor = communalSettingsInteractor; mStatusBarStateController = statusBarStateController; statusBarStateController.addCallback(this); @@ -2429,9 +2433,18 @@ public class KeyguardViewMediator implements CoreStartable, private void doKeyguardLocked(Bundle options) { // If the power button behavior requests to open the glanceable hub. if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) { - // Set the hub to show immediately when the SysUI window shows, then continue to lock - // the device. - mCommunalSceneInteractor.get().showHubFromPowerButton(); + if (mCommunalSettingsInteractor.get().getAutoOpenEnabled().getValue()) { + // Set the hub to show immediately when the SysUI window shows, then continue to + // lock the device. + mCommunalSceneInteractor.get().showHubFromPowerButton(); + } else { + // If the hub is not available, go to sleep instead of locking. This can happen + // because the power button behavior does not check all possible reasons the hub + // might be disabled. + mPM.goToSleep(android.os.SystemClock.uptimeMillis(), + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0); + return; + } } int currentUserId = mSelectedUserInteractor.getSelectedUserId(); @@ -3765,13 +3778,7 @@ public class KeyguardViewMediator implements CoreStartable, Log.d(TAG, "Status bar manager is disabled for visible background users"); } } else { - try { - mStatusBarService.disableForUser(flags, mStatusBarDisableToken, - mContext.getPackageName(), - mSelectedUserInteractor.getSelectedUserId()); - } catch (RemoteException e) { - Log.d(TAG, "Failed to force clear flags", e); - } + statusBarServiceDisableForUser(flags, "Failed to force clear flags"); } } @@ -3807,18 +3814,29 @@ public class KeyguardViewMediator implements CoreStartable, // Handled in StatusBarDisableFlagsInteractor. if (!KeyguardWmStateRefactor.isEnabled()) { - try { - mStatusBarService.disableForUser(flags, mStatusBarDisableToken, - mContext.getPackageName(), - mSelectedUserInteractor.getSelectedUserId()); - } catch (RemoteException e) { - Log.d(TAG, "Failed to set disable flags: " + flags, e); - } + statusBarServiceDisableForUser(flags, "Failed to set disable flags: "); } } } } + private void statusBarServiceDisableForUser(int flags, String loggingContext) { + Runnable runnable = () -> { + try { + mStatusBarService.disableForUser(flags, mStatusBarDisableToken, + mContext.getPackageName(), + mSelectedUserInteractor.getSelectedUserId()); + } catch (RemoteException e) { + Log.d(TAG, loggingContext + " " + flags, e); + } + }; + if (com.android.systemui.Flags.bouncerUiRevamp()) { + mUiBgExecutor.execute(runnable); + } else { + runnable.run(); + } + } + /** * Handle message sent by {@link #resetStateLocked} * @see #RESET diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt index ddccc5d9e96d..41d14b9e727f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt @@ -20,7 +20,16 @@ import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils -/** Helper for reading or using the keyguard wm state refactor flag state. */ +/** + * Helper for reading or using the keyguard_wm_state_refactor flag state. + * + * keyguard_wm_state_refactor works both with and without flexiglass (scene_container), but + * flexiglass requires keyguard_wm_state_refactor. For this reason, this class will return isEnabled + * if either keyguard_wm_state_refactor OR scene_container are enabled. This enables us to roll out + * keyguard_wm_state_refactor independently of scene_container, while also ensuring that + * scene_container rolling out ahead of keyguard_wm_state_refactor causes code gated by + * KeyguardWmStateRefactor to be enabled as well. + */ @Suppress("NOTHING_TO_INLINE") object KeyguardWmStateRefactor { /** The aconfig flag name */ @@ -30,10 +39,9 @@ object KeyguardWmStateRefactor { val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) - /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.keyguardWmStateRefactor() + get() = Flags.keyguardWmStateRefactor() || Flags.sceneContainer() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 6b1248b6983e..1fe6eb9ce7c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -42,6 +42,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor; import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; @@ -182,6 +183,7 @@ public interface KeyguardModule { KeyguardInteractor keyguardInteractor, KeyguardTransitionBootInteractor transitionBootInteractor, Lazy<CommunalSceneInteractor> communalSceneInteractor, + Lazy<CommunalSettingsInteractor> communalSettingsInteractor, WindowManagerOcclusionManager windowManagerOcclusionManager) { return new KeyguardViewMediator( context, @@ -234,6 +236,7 @@ public interface KeyguardModule { keyguardInteractor, transitionBootInteractor, communalSceneInteractor, + communalSettingsInteractor, windowManagerOcclusionManager); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index b889c3e61837..9f04f69bd0bf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -19,6 +19,8 @@ package com.android.systemui.qs.composefragment import android.annotation.SuppressLint import android.content.Context import android.content.res.Configuration +import android.graphics.Canvas +import android.graphics.Path import android.graphics.PointF import android.graphics.Rect import android.os.Bundle @@ -125,7 +127,6 @@ import com.android.systemui.qs.composefragment.SceneKeys.debugName import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams -import com.android.systemui.qs.composefragment.ui.notificationScrimClip import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings import com.android.systemui.qs.composefragment.ui.toEditMode import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel @@ -241,7 +242,7 @@ constructor( FrameLayoutTouchPassthrough( context, { notificationScrimClippingParams.isEnabled }, - { notificationScrimClippingParams.params.top }, + snapshotFlow { notificationScrimClippingParams.params }, // Only allow scrolling when we are fully expanded. That way, we don't intercept // swipes in lockscreen (when somehow QS is receiving touches). { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing }, @@ -276,11 +277,6 @@ constructor( } } .graphicsLayer { alpha = viewModel.viewAlpha } - .thenIf(notificationScrimClippingParams.isEnabled) { - Modifier.notificationScrimClip { - notificationScrimClippingParams.params - } - } .thenIf(!Flags.notificationShadeBlur()) { Modifier.offset { IntOffset( @@ -1061,17 +1057,75 @@ private const val EDIT_MODE_TIME_MILLIS = 500 private class FrameLayoutTouchPassthrough( context: Context, private val clippingEnabledProvider: () -> Boolean, - private val clippingTopProvider: () -> Int, + private val clippingParams: Flow<NotificationScrimClipParams>, private val canScrollForwardQs: () -> Boolean, private val emitMotionEventForFalsing: () -> Unit, ) : FrameLayout(context) { + + init { + repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + clippingParams.collect { currentClipParams = it } + } + } + } + + private val currentClippingPath = Path() + private var lastWidth = -1 + set(value) { + if (field != value) { + field = value + updateClippingPath() + } + } + + private var currentClipParams = NotificationScrimClipParams() + set(value) { + if (field != value) { + field = value + updateClippingPath() + } + } + + private fun updateClippingPath() { + currentClippingPath.rewind() + if (clippingEnabledProvider()) { + val right = width + currentClipParams.rightInset + val left = -currentClipParams.leftInset + val top = currentClipParams.top + val bottom = currentClipParams.bottom + currentClippingPath.addRoundRect( + left.toFloat(), + top.toFloat(), + right.toFloat(), + bottom.toFloat(), + currentClipParams.radius.toFloat(), + currentClipParams.radius.toFloat(), + Path.Direction.CW, + ) + } + invalidate() + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + lastWidth = right - left + } + + override fun dispatchDraw(canvas: Canvas) { + if (!currentClippingPath.isEmpty) { + canvas.clipOutPath(currentClippingPath) + } + super.dispatchDraw(canvas) + } + override fun isTransformedTouchPointInView( x: Float, y: Float, child: View?, outLocalPoint: PointF?, ): Boolean { - return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) { + return if (clippingEnabledProvider() && y + translationY > currentClipParams.top) { false } else { super.isTransformedTouchPointInView(x, y, child, outLocalPoint) diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt deleted file mode 100644 index 3049a40f18c4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.composefragment.ui - -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.ClipOp -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.drawscope.clipRect -import androidx.compose.ui.graphics.graphicsLayer - -/** - * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out - * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)` - * from the QS container. - */ -fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier { - return this.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } - .drawWithContent { - drawContent() - val params = clipParams() - val left = -params.leftInset.toFloat() - val right = size.width + params.rightInset.toFloat() - val top = params.top.toFloat() - val bottom = params.bottom.toFloat() - val clipSize = Size(right - left, bottom - top) - if (!clipSize.isEmpty()) { - clipRect { - drawRoundRect( - color = Color.Black, - cornerRadius = CornerRadius(params.radius.toFloat()), - blendMode = BlendMode.Clear, - topLeft = Offset(left, top), - size = Size(right - left, bottom - top), - ) - } - } - } -} - -/** Params for [notificationScrimClip]. */ -data class NotificationScrimClipParams( - val top: Int = 0, - val bottom: Int = 0, - val leftInset: Int = 0, - val rightInset: Int = 0, - val radius: Int = 0, -) diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt new file mode 100644 index 000000000000..db320d3b9f1c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.composefragment.ui + +/** Params for [notificationScrimClip]. */ +data class NotificationScrimClipParams( + val top: Int = 0, + val bottom: Int = 0, + val leftInset: Int = 0, + val rightInset: Int = 0, + val radius: Int = 0, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index fce5a16bd85d..e292bcf1f7a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -24,6 +24,7 @@ import android.util.IndentingPrintWriter import android.util.Log import android.util.MathUtils import android.view.Choreographer +import android.view.Display import android.view.View import androidx.annotation.VisibleForTesting import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -42,7 +43,9 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK import com.android.systemui.statusbar.phone.DozeParameters @@ -52,6 +55,7 @@ import com.android.systemui.util.WallpaperController import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.wm.shell.appzoomout.AppZoomOut +import dagger.Lazy import java.io.PrintWriter import java.util.Optional import javax.inject.Inject @@ -83,6 +87,7 @@ constructor( private val appZoomOutOptional: Optional<AppZoomOut>, @Application private val applicationScope: CoroutineScope, dumpManager: DumpManager, + private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>, ) : ShadeExpansionListener, Dumpable { companion object { private const val WAKE_UP_ANIMATION_ENABLED = true @@ -228,6 +233,14 @@ constructor( private data class WakeAndUnlockBlurData(val radius: Float, val useZoom: Boolean = true) + private val isShadeOnDefaultDisplay: Boolean + get() = + if (ShadeWindowGoesAround.isEnabled) { + shadeDisplaysRepository.get().displayId.value == Display.DEFAULT_DISPLAY + } else { + true + } + /** Blur radius of the wake and unlock animation on this frame, and whether to zoom out. */ private var wakeAndUnlockBlurData = WakeAndUnlockBlurData(0f) set(value) { @@ -265,9 +278,14 @@ constructor( var blur = shadeRadius.toInt() // If the blur comes from waking up, we don't want to zoom out the background val zoomOut = - if (shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom) - blurRadiusToZoomOut(blurRadius = shadeRadius) - else 0f + when { + // When the shade is in another display, we don't want to zoom out the background. + // Only the default display is supported right now. + !isShadeOnDefaultDisplay -> 0f + shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom -> + blurRadiusToZoomOut(blurRadius = shadeRadius) + else -> 0f + } // Make blur be 0 if it is necessary to stop blur effect. if (scrimsVisible) { if (!Flags.notificationShadeBlur()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 206654abcaaa..9b314f25e02b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -1500,6 +1500,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mKosmos.getKeyguardInteractor(), mKeyguardTransitionBootInteractor, mKosmos::getCommunalSceneInteractor, + mKosmos::getCommunalSettingsInteractor, mock(WindowManagerOcclusionManager.class)); mViewMediator.mUserChangedCallback = mUserTrackerCallback; mViewMediator.start(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt index 86f7966d4ada..d6b778fe2bc2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt @@ -35,8 +35,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.classifier.falsingCollector +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel import com.android.systemui.concurrency.fakeExecutor @@ -81,8 +87,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.verify /** Kotlin version of KeyguardViewMediatorTest to allow for coroutine testing. */ @SmallTest @@ -152,6 +161,7 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { keyguardInteractor, keyguardTransitionBootInteractor, { communalSceneInteractor }, + { communalSettingsInteractor }, mock<WindowManagerOcclusionManager>(), ) } @@ -164,6 +174,10 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { @Test fun doKeyguardTimeout_changesCommunalScene() = kosmos.runTest { + // Hub is enabled and hub condition is active. + setCommunalV2Enabled(true) + enableHubOnCharging() + // doKeyguardTimeout message received. val timeoutOptions = Bundle() timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true) @@ -174,4 +188,56 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { assertThat(communalSceneRepository.currentScene.value) .isEqualTo(CommunalScenes.Communal) } + + @Test + fun doKeyguardTimeout_communalNotAvailable_sleeps() = + kosmos.runTest { + // Hub disabled. + setCommunalV2Enabled(false) + + // doKeyguardTimeout message received. + val timeoutOptions = Bundle() + timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true) + underTest.doKeyguardTimeout(timeoutOptions) + testableLooper.processAllMessages() + + // Sleep is requested. + verify(powerManager) + .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0)) + + // Hub scene is not changed. + assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) + } + + @Test + fun doKeyguardTimeout_hubConditionNotActive_sleeps() = + kosmos.runTest { + // Communal enabled, but hub condition set to never. + setCommunalV2Enabled(true) + disableHubShowingAutomatically() + + // doKeyguardTimeout message received. + val timeoutOptions = Bundle() + timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true) + underTest.doKeyguardTimeout(timeoutOptions) + testableLooper.processAllMessages() + + // Sleep is requested. + verify(powerManager) + .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0)) + + // Hub scene is not changed. + assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) + } + + private fun Kosmos.enableHubOnCharging() { + communalSettingsInteractor.setSuppressionReasons(emptyList()) + batteryRepository.fake.setDevicePluggedIn(true) + } + + private fun Kosmos.disableHubShowingAutomatically() { + communalSettingsInteractor.setSuppressionReasons( + listOf(SuppressionReason.ReasonUnknown(FEATURE_AUTO_OPEN)) + ) + } } diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 0b9c45de6e40..60343e9e81e5 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -152,9 +152,20 @@ public class AutoclickController extends BaseEventStreamTransformation { if (direction == AutoclickScrollPanel.DIRECTION_EXIT) { return; } - // For direction buttons, perform scroll action immediately. - if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) { - handleScroll(direction); + + // Handle all non-exit buttons when hovered. + if (hovered) { + // Clear the indicator. + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.cancel(); + if (mAutoclickIndicatorView != null) { + mAutoclickIndicatorView.clearIndicator(); + } + } + // Perform scroll action. + if (direction != DIRECTION_NONE) { + handleScroll(direction); + } } } }; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index a28069bbf050..95e58e1a7300 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -684,8 +684,9 @@ public final class DisplayManagerService extends SystemService { final var backupManager = new BackupManager(mContext); Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> topologyChangedCallback = update -> { - if (mInputManagerInternal != null) { - mInputManagerInternal.setDisplayTopology(update.second); + DisplayTopologyGraph graph = update.second; + if (mInputManagerInternal != null && graph != null) { + mInputManagerInternal.setDisplayTopology(graph); } deliverTopologyUpdate(update.first); }; @@ -3647,7 +3648,7 @@ public final class DisplayManagerService extends SystemService { private void deliverTopologyUpdate(DisplayTopology topology) { if (DEBUG) { - Slog.d(TAG, "Delivering topology update"); + Slog.d(TAG, "Delivering topology update: " + topology); } if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { Trace.instant(Trace.TRACE_TAG_POWER, "deliverTopologyUpdate"); @@ -4209,13 +4210,18 @@ public final class DisplayManagerService extends SystemService { public boolean mWifiDisplayScanRequested; - // A single pending event. + // A single pending display event. private record Event(int displayId, @DisplayEvent int event) { }; - // The list of pending events. This is null until there is a pending event to be saved. - // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. + // The list of pending display events. This is null until there is a pending event to be + // saved. This is only used if {@link deferDisplayEventsWhenFrozen()} is true. + @GuardedBy("mCallback") + @Nullable + private ArrayList<Event> mPendingDisplayEvents; + @GuardedBy("mCallback") - private ArrayList<Event> mPendingEvents; + @Nullable + private DisplayTopology mPendingTopology; // Process states: a process is ready to receive events if it is neither cached nor // frozen. @@ -4285,7 +4291,10 @@ public final class DisplayManagerService extends SystemService { */ @GuardedBy("mCallback") private boolean hasPendingAndIsReadyLocked() { - return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty() && mAlive; + boolean pendingDisplayEvents = mPendingDisplayEvents != null + && !mPendingDisplayEvents.isEmpty(); + boolean pendingTopology = mPendingTopology != null; + return isReadyLocked() && (pendingDisplayEvents || pendingTopology) && mAlive; } /** @@ -4366,7 +4375,8 @@ public final class DisplayManagerService extends SystemService { // occurs as the client is transitioning to ready but pending events have not // been dispatched. The new event must be added to the pending list to // preserve event ordering. - if (!isReadyLocked() || (mPendingEvents != null && !mPendingEvents.isEmpty())) { + if (!isReadyLocked() || (mPendingDisplayEvents != null + && !mPendingDisplayEvents.isEmpty())) { // The client is interested in the event but is not ready to receive it. // Put the event on the pending list. addDisplayEvent(displayId, event); @@ -4453,13 +4463,13 @@ public final class DisplayManagerService extends SystemService { // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. @GuardedBy("mCallback") private void addDisplayEvent(int displayId, int event) { - if (mPendingEvents == null) { - mPendingEvents = new ArrayList<>(); + if (mPendingDisplayEvents == null) { + mPendingDisplayEvents = new ArrayList<>(); } - if (!mPendingEvents.isEmpty()) { + if (!mPendingDisplayEvents.isEmpty()) { // Ignore redundant events. Further optimization is possible by merging adjacent // events. - Event last = mPendingEvents.get(mPendingEvents.size() - 1); + Event last = mPendingDisplayEvents.get(mPendingDisplayEvents.size() - 1); if (last.displayId == displayId && last.event == event) { if (DEBUG) { Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event @@ -4468,12 +4478,13 @@ public final class DisplayManagerService extends SystemService { return; } } - mPendingEvents.add(new Event(displayId, event)); + mPendingDisplayEvents.add(new Event(displayId, event)); } /** * @return {@code false} if RemoteException happens; otherwise {@code true} for - * success. + * success. This returns true even if the update was deferred because the remote client is + * cached or frozen. */ boolean notifyTopologyUpdateAsync(DisplayTopology topology) { if ((mInternalEventFlagsMask.get() @@ -4490,6 +4501,18 @@ public final class DisplayManagerService extends SystemService { // The client is not interested in this event, so do nothing. return true; } + + if (deferDisplayEventsWhenFrozen()) { + synchronized (mCallback) { + // Save the new update if the client frozen or cached (not ready). + if (!isReadyLocked()) { + // The client is interested in the update but is not ready to receive it. + mPendingTopology = topology; + return true; + } + } + } + return transmitTopologyUpdate(topology); } @@ -4514,37 +4537,54 @@ public final class DisplayManagerService extends SystemService { // would be unusual to do so. The method returns true on success. // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. public boolean dispatchPending() { - Event[] pending; + Event[] pendingDisplayEvents = null; + DisplayTopology pendingTopology; synchronized (mCallback) { - if (mPendingEvents == null || mPendingEvents.isEmpty() || !mAlive) { + if (!mAlive) { return true; } if (!isReadyLocked()) { return false; } - pending = new Event[mPendingEvents.size()]; - pending = mPendingEvents.toArray(pending); - mPendingEvents.clear(); + + if (mPendingDisplayEvents != null && !mPendingDisplayEvents.isEmpty()) { + pendingDisplayEvents = new Event[mPendingDisplayEvents.size()]; + pendingDisplayEvents = mPendingDisplayEvents.toArray(pendingDisplayEvents); + mPendingDisplayEvents.clear(); + } + + pendingTopology = mPendingTopology; + mPendingTopology = null; } try { - for (int i = 0; i < pending.length; i++) { - Event displayEvent = pending[i]; - if (DEBUG) { - Slog.d(TAG, "Send pending display event #" + i + " " - + displayEvent.displayId + "/" - + displayEvent.event + " to " + mUid + "/" + mPid); - } + if (pendingDisplayEvents != null) { + for (int i = 0; i < pendingDisplayEvents.length; i++) { + Event displayEvent = pendingDisplayEvents[i]; + if (DEBUG) { + Slog.d(TAG, "Send pending display event #" + i + " " + + displayEvent.displayId + "/" + + displayEvent.event + " to " + mUid + "/" + mPid); + } + + if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) { + continue; + } - if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) { - continue; + transmitDisplayEvent(displayEvent.displayId, displayEvent.event); } + } - transmitDisplayEvent(displayEvent.displayId, displayEvent.event); + if (pendingTopology != null) { + if (DEBUG) { + Slog.d(TAG, "Send pending topology: " + pendingTopology + + " to " + mUid + "/" + mPid); + } + mCallback.onTopologyChanged(pendingTopology); } + return true; } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify process " - + mPid + " that display topology changed, assuming it died.", ex); + Slog.w(TAG, "Failed to notify process " + mPid + ", assuming it died.", ex); binderDied(); return false; @@ -4556,11 +4596,12 @@ public final class DisplayManagerService extends SystemService { if (deferDisplayEventsWhenFrozen()) { final String fmt = "mPid=%d mUid=%d mWifiDisplayScanRequested=%s" - + " cached=%s frozen=%s pending=%d"; + + " cached=%s frozen=%s pendingDisplayEvents=%d pendingTopology=%b"; synchronized (mCallback) { return formatSimple(fmt, mPid, mUid, mWifiDisplayScanRequested, mCached, mFrozen, - (mPendingEvents == null) ? 0 : mPendingEvents.size()); + (mPendingDisplayEvents == null) ? 0 : mPendingDisplayEvents.size(), + mPendingTopology != null); } } else { final String fmt = diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 58cf29b59961..c174451e8f5b 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -192,9 +192,15 @@ public class MediaSessionService extends SystemService implements Monitor { private final Map<Integer, Set<MediaSessionRecordImpl>> mUserEngagedSessionsForFgs = new HashMap<>(); - /* Maps uid with all media notifications associated to it */ + /** + * Maps UIDs to their associated media notifications: UID -> (Notification ID -> + * {@link android.service.notification.StatusBarNotification}). + * Each UID maps to a collection of notifications, identified by their + * {@link android.service.notification.StatusBarNotification#getId()}. + */ @GuardedBy("mLock") - private final Map<Integer, Set<StatusBarNotification>> mMediaNotifications = new HashMap<>(); + private final Map<Integer, Map<String, StatusBarNotification>> mMediaNotifications = + new HashMap<>(); // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile) // It's always not null after the MediaSessionService is started. @@ -737,7 +743,8 @@ public class MediaSessionService extends SystemService implements Monitor { } synchronized (mLock) { int uid = mediaSessionRecord.getUid(); - for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid, Set.of())) { + for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid, + Map.of()).values()) { if (mediaSessionRecord.isLinkedToNotification(sbn.getNotification())) { setFgsActiveLocked(mediaSessionRecord, sbn); return; @@ -771,7 +778,7 @@ public class MediaSessionService extends SystemService implements Monitor { int uid, MediaSessionRecordImpl record) { synchronized (mLock) { for (StatusBarNotification sbn : - mMediaNotifications.getOrDefault(uid, Set.of())) { + mMediaNotifications.getOrDefault(uid, Map.of()).values()) { if (record.isLinkedToNotification(sbn.getNotification())) { return sbn; } @@ -794,7 +801,8 @@ public class MediaSessionService extends SystemService implements Monitor { for (MediaSessionRecordImpl record : mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { for (StatusBarNotification sbn : - mMediaNotifications.getOrDefault(uid, Set.of())) { + mMediaNotifications.getOrDefault(uid, Map.of()).values()) { + // if (record.isLinkedToNotification(sbn.getNotification())) { // A user engaged session linked with a media notification is found. // We shouldn't call stop FGS in this case. @@ -3262,8 +3270,12 @@ public class MediaSessionService extends SystemService implements Monitor { return; } synchronized (mLock) { - mMediaNotifications.putIfAbsent(uid, new HashSet<>()); - mMediaNotifications.get(uid).add(sbn); + Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid); + if (notifications == null) { + notifications = new HashMap<>(); + mMediaNotifications.put(uid, notifications); + } + notifications.put(sbn.getKey(), sbn); MediaSessionRecordImpl userEngagedRecord = getUserEngagedMediaSessionRecordForNotification(uid, postedNotification); if (userEngagedRecord != null) { @@ -3287,10 +3299,10 @@ public class MediaSessionService extends SystemService implements Monitor { return; } synchronized (mLock) { - Set<StatusBarNotification> uidMediaNotifications = mMediaNotifications.get(uid); - if (uidMediaNotifications != null) { - uidMediaNotifications.remove(sbn); - if (uidMediaNotifications.isEmpty()) { + Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid); + if (notifications != null) { + notifications.remove(sbn.getKey()); + if (notifications.isEmpty()) { mMediaNotifications.remove(uid); } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index fcd1452b8702..2744721c3a46 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -311,6 +311,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6; static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7; static final int SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP = 8; + static final int SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP = 9; // must match: config_LongPressOnPowerBehavior in config.xml // The config value can be overridden using Settings.Global.POWER_BUTTON_LONG_PRESS @@ -1234,8 +1235,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; } case SHORT_PRESS_POWER_DREAM_OR_SLEEP: { - attemptToDreamFromShortPowerButtonPress( - true, + attemptToDreamOrAwakeFromShortPowerButtonPress( + /* isScreenOn */ true, + /* awakeWhenDream */ false, + /* noDreamAction */ () -> sleepDefaultDisplayFromPowerButton(eventTime, 0)); break; } @@ -1269,13 +1272,22 @@ public class PhoneWindowManager implements WindowManagerPolicy { lockNow(options); } else { // If the hub cannot be run, attempt to dream instead. - attemptToDreamFromShortPowerButtonPress( + attemptToDreamOrAwakeFromShortPowerButtonPress( /* isScreenOn */ true, + /* awakeWhenDream */ false, /* noDreamAction */ () -> sleepDefaultDisplayFromPowerButton(eventTime, 0)); } break; } + case SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP: { + attemptToDreamOrAwakeFromShortPowerButtonPress( + /* isScreenOn */ true, + /* awakeWhenDream */ true, + /* noDreamAction */ + () -> sleepDefaultDisplayFromPowerButton(eventTime, 0)); + break; + } } } } @@ -1319,15 +1331,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { } /** - * Attempt to dream from a power button press. + * Attempt to dream, awake or sleep from a power button press. * * @param isScreenOn Whether the screen is currently on. + * @param awakeWhenDream When it's set to {@code true}, awake the device from dreaming. + * Otherwise, go to sleep. * @param noDreamAction The action to perform if dreaming is not possible. */ - private void attemptToDreamFromShortPowerButtonPress( - boolean isScreenOn, Runnable noDreamAction) { + private void attemptToDreamOrAwakeFromShortPowerButtonPress( + boolean isScreenOn, boolean awakeWhenDream, Runnable noDreamAction) { if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP - && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) { + && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP + && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP) { // If the power button behavior isn't one that should be able to trigger the dream, give // up. noDreamAction.run(); @@ -1335,9 +1350,24 @@ public class PhoneWindowManager implements WindowManagerPolicy { } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); - if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) { - Slog.d(TAG, "Can't start dreaming when attempting to dream from short power" - + " press (isScreenOn=" + isScreenOn + ")"); + if (dreamManagerInternal == null) { + Slog.d(TAG, + "Can't access dream manager dreaming when attempting to start or stop dream " + + "from short power press (isScreenOn=" + + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")"); + noDreamAction.run(); + return; + } + + if (!dreamManagerInternal.canStartDreaming(isScreenOn)) { + if (awakeWhenDream && dreamManagerInternal.isDreaming()) { + dreamManagerInternal.stopDream(false /*immediate*/, "short press power" /*reason*/); + return; + } + Slog.d(TAG, + "Can't start dreaming and the device is not dreaming when attempting to start " + + "or stop dream from short power press (isScreenOn=" + + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")"); noDreamAction.run(); return; } @@ -2312,6 +2342,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { return ActivityManager.getService(); } + LockPatternUtils getLockPatternUtils() { + return new LockPatternUtils(mContext); + } + ButtonOverridePermissionChecker getButtonOverridePermissionChecker() { return new ButtonOverridePermissionChecker(); } @@ -2360,7 +2394,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mAccessibilityShortcutController = injector.getAccessibilityShortcutController( mContext, new Handler(), mCurrentUserId); mGlobalActionsFactory = injector.getGlobalActionsFactory(); - mLockPatternUtils = new LockPatternUtils(mContext); + mLockPatternUtils = injector.getLockPatternUtils(); mLogger = new MetricsLogger(); Resources res = mContext.getResources(); diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index ac4aac694c3a..11edb93dffea 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -383,7 +383,9 @@ public class MetricUtilities { /* api_name */ initialPhaseMetric.getApiName(), /* primary_candidates_indicated */ - candidatePrimaryProviderList + candidatePrimaryProviderList, + /* api_prepared */ + initialPhaseMetric.hasApiUsedPrepareFlow() ); } catch (Exception e) { Slog.w(TAG, "Unexpected error during candidate provider uid metric emit: " + e); @@ -442,7 +444,9 @@ public class MetricUtilities { /* autofill_session_id */ initialPhaseMetric.getAutofillSessionId(), /* autofill_request_id */ - initialPhaseMetric.getAutofillRequestId() + initialPhaseMetric.getAutofillRequestId(), + /* api_prepared */ + initialPhaseMetric.hasApiUsedPrepareFlow() ); } catch (Exception e) { Slog.w(TAG, "Unexpected error during initial metric emit: " + e); diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index d60807c7b001..2d4360edf3a8 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -27,6 +27,7 @@ import android.credentials.GetCredentialRequest; import android.credentials.IGetCredentialCallback; import android.credentials.IPrepareGetCredentialCallback; import android.credentials.PrepareGetCredentialResponseInternal; +import android.credentials.flags.Flags; import android.credentials.selection.GetCredentialProviderData; import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; @@ -60,7 +61,12 @@ public class PrepareGetRequestSession extends GetRequestSession { int numTypes = (request.getCredentialOptions().stream() .map(CredentialOption::getType).collect( Collectors.toSet())).size(); // Dedupe type strings - mRequestSessionMetric.collectGetFlowInitialMetricInfo(request); + if (!Flags.fixMetricDuplicationEmits()) { + mRequestSessionMetric.collectGetFlowInitialMetricInfo(request); + } else { + mRequestSessionMetric.collectGetFlowInitialMetricInfo(request, + /*isApiPrepared=*/ true); + } mPrepareGetCredentialCallback = prepareGetCredentialCallback; Slog.i(TAG, "PrepareGetRequestSession constructed."); diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java index 8a4e86c440b3..811b97a5bf03 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java @@ -55,6 +55,9 @@ public class InitialPhaseMetric { // The request id of autofill if the request is from autofill, defaults to -1 private int mAutofillRequestId = -1; + // Indicates if this API call used the prepare flow, defaults to false + private boolean mApiUsedPrepareFlow = false; + public InitialPhaseMetric(int sessionIdTrackOne) { mSessionIdCaller = sessionIdTrackOne; @@ -173,4 +176,17 @@ public class InitialPhaseMetric { public int[] getUniqueRequestCounts() { return mRequestCounts.values().stream().mapToInt(Integer::intValue).toArray(); } + + /* ------ API Prepared ------ */ + + public void setApiUsedPrepareFlow(boolean apiUsedPrepareFlow) { + mApiUsedPrepareFlow = apiUsedPrepareFlow; + } + + /** + * @return a boolean indicating if this API call utilized a prepare flow + */ + public boolean hasApiUsedPrepareFlow() { + return mApiUsedPrepareFlow; + } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index 619a56846e95..dc1747f803ea 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -225,6 +225,22 @@ public class RequestSessionMetric { } /** + * Collects initializations for Get flow metrics. + * + * @param request the get credential request containing information to parse for metrics + * @param isApiPrepared indicates this API flow utilized the 'prepare' flow + */ + public void collectGetFlowInitialMetricInfo(GetCredentialRequest request, + boolean isApiPrepared) { + try { + collectGetFlowInitialMetricInfo(request); + mInitialPhaseMetric.setApiUsedPrepareFlow(isApiPrepared); + } catch (Exception e) { + Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e); + } + } + + /** * During browsing, where multiple entries can be selected, this collects the browsing phase * metric information. This is emitted together with the final phase, and the recursive path * with authentication entries, which may occur in rare circumstances, are captured. diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp index 36ea24195789..c85053d13e68 100644 --- a/services/tests/displayservicetests/Android.bp +++ b/services/tests/displayservicetests/Android.bp @@ -51,6 +51,7 @@ android_test { data: [ ":DisplayManagerTestApp", + ":TopologyTestApp", ], certificate: "platform", diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml index 205ff058275a..76f219b7433b 100644 --- a/services/tests/displayservicetests/AndroidManifest.xml +++ b/services/tests/displayservicetests/AndroidManifest.xml @@ -29,6 +29,7 @@ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <uses-permission android:name="android.permission.MANAGE_USB" /> + <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> <!-- Permissions needed for DisplayTransformManagerTest --> <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" /> diff --git a/services/tests/displayservicetests/AndroidTest.xml b/services/tests/displayservicetests/AndroidTest.xml index f3697bbffd5c..2fe37233870f 100644 --- a/services/tests/displayservicetests/AndroidTest.xml +++ b/services/tests/displayservicetests/AndroidTest.xml @@ -28,6 +28,7 @@ <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> <option name="test-file-name" value="DisplayManagerTestApp.apk" /> + <option name="test-file-name" value="TopologyTestApp.apk" /> </target_preparer> <option name="test-tag" value="DisplayServiceTests" /> diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java index 1f45792e5097..bf4b61347bab 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java @@ -16,7 +16,6 @@ package com.android.server.display; -import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; import static android.util.DisplayMetrics.DENSITY_HIGH; @@ -27,19 +26,11 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; -import android.app.ActivityManager; -import android.app.Instrumentation; -import android.content.Context; import android.content.Intent; -import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; -import android.os.BinderProxy; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.os.Messenger; -import android.platform.test.annotations.AppModeSdkSandbox; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -48,10 +39,7 @@ import android.util.SparseArray; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; -import androidx.test.platform.app.InstrumentationRegistry; -import com.android.compatibility.common.util.SystemUtil; -import com.android.compatibility.common.util.TestUtils; import com.android.server.am.Flags; import org.junit.After; @@ -63,9 +51,7 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; -import java.io.IOException; import java.util.Arrays; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -73,8 +59,7 @@ import java.util.concurrent.TimeUnit; * Tests that applications can receive display events correctly. */ @RunWith(Parameterized.class) -@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") -public class DisplayEventDeliveryTest { +public class DisplayEventDeliveryTest extends EventDeliveryTestBase { private static final String TAG = "DisplayEventDeliveryTest"; @Rule @@ -85,37 +70,17 @@ public class DisplayEventDeliveryTest { private static final int WIDTH = 720; private static final int HEIGHT = 480; - private static final int MESSAGE_LAUNCHED = 1; - private static final int MESSAGE_CALLBACK = 2; - private static final int DISPLAY_ADDED = 1; private static final int DISPLAY_CHANGED = 2; private static final int DISPLAY_REMOVED = 3; - private static final long DISPLAY_EVENT_TIMEOUT_MSEC = 100; - private static final long TEST_FAILURE_TIMEOUT_MSEC = 10000; - private static final String TEST_PACKAGE = "com.android.servicestests.apps.displaymanagertestapp"; private static final String TEST_ACTIVITY = TEST_PACKAGE + ".DisplayEventActivity"; private static final String TEST_DISPLAYS = "DISPLAYS"; - private static final String TEST_MESSENGER = "MESSENGER"; private final Object mLock = new Object(); - private Instrumentation mInstrumentation; - private Context mContext; - private DisplayManager mDisplayManager; - private ActivityManager mActivityManager; - private ActivityManager.OnUidImportanceListener mUidImportanceListener; - private CountDownLatch mLatchActivityLaunch; - private CountDownLatch mLatchActivityCached; - private HandlerThread mHandlerThread; - private Handler mHandler; - private Messenger mMessenger; - private int mPid; - private int mUid; - /** * Array of DisplayBundle. The test handler uses it to check if certain display events have * been sent to DisplayEventActivity. @@ -167,7 +132,7 @@ public class DisplayEventDeliveryTest { */ public void assertNoDisplayEvents() { try { - assertNull(mExpectations.poll(DISPLAY_EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)); + assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -239,37 +204,17 @@ public class DisplayEventDeliveryTest { } @Before - public void setUp() throws Exception { - mInstrumentation = InstrumentationRegistry.getInstrumentation(); - mContext = mInstrumentation.getContext(); - mDisplayManager = mContext.getSystemService(DisplayManager.class); - mLatchActivityLaunch = new CountDownLatch(1); - mLatchActivityCached = new CountDownLatch(1); - mActivityManager = mContext.getSystemService(ActivityManager.class); - mUidImportanceListener = (uid, importance) -> { - if (uid == mUid && importance == IMPORTANCE_CACHED) { - Log.d(TAG, "Listener " + uid + " becomes " + importance); - mLatchActivityCached.countDown(); - } - }; - SystemUtil.runWithShellPermissionIdentity(() -> - mActivityManager.addOnUidImportanceListener(mUidImportanceListener, - IMPORTANCE_CACHED)); + public void setUp() { + super.setUp(); // The lock is not functionally necessary but eliminates lint error messages. synchronized (mLock) { mDisplayBundles = new SparseArray<>(); } - mHandlerThread = new HandlerThread("handler"); - mHandlerThread.start(); - mHandler = new TestHandler(mHandlerThread.getLooper()); - mMessenger = new Messenger(mHandler); - mPid = 0; } @After public void tearDown() throws Exception { - mActivityManager.removeOnUidImportanceListener(mUidImportanceListener); - mHandlerThread.quitSafely(); + super.tearDown(); synchronized (mLock) { for (int i = 0; i < mDisplayBundles.size(); i++) { DisplayBundle bundle = mDisplayBundles.valueAt(i); @@ -278,7 +223,31 @@ public class DisplayEventDeliveryTest { } mDisplayBundles.clear(); } - SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + TEST_PACKAGE); + } + + @Override + protected String getTag() { + return TAG; + } + + @Override + protected Handler getHandler(Looper looper) { + return new TestHandler(looper); + } + + @Override + protected String getTestPackage() { + return TEST_PACKAGE; + } + + @Override + protected String getTestActivity() { + return TEST_ACTIVITY; + } + + @Override + protected void putExtra(Intent intent) { + intent.putExtra(TEST_DISPLAYS, mDisplayCount); } /** @@ -291,42 +260,8 @@ public class DisplayEventDeliveryTest { } /** - * Return true if the freezer is enabled on this platform and if freezer notifications are - * supported. It is not enough to test that the freezer notification feature is enabled - * because some devices do not have the necessary kernel support. - */ - private boolean isAppFreezerEnabled() { - try { - return mActivityManager.getService().isAppFreezerEnabled() - && android.os.Flags.binderFrozenStateChangeCallback() - && BinderProxy.isFrozenStateChangeCallbackSupported(); - } catch (Exception e) { - Log.e(TAG, "isAppFreezerEnabled() failed: " + e); - return false; - } - } - - private void waitForProcessFreeze(int pid, long timeoutMs) { - // TODO: Add a listener to monitor freezer state changes. - SystemUtil.runWithShellPermissionIdentity(() -> { - TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid, - (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), - () -> mActivityManager.isProcessFrozen(pid)); - }); - } - - private void waitForProcessUnfreeze(int pid, long timeoutMs) { - // TODO: Add a listener to monitor freezer state changes. - SystemUtil.runWithShellPermissionIdentity(() -> { - TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid, - (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), - () -> !mActivityManager.isProcessFrozen(pid)); - }); - } - - /** - * Create virtual displays, change their configurations and release them. The number of - * displays is set by the {@link #mDisplays} variable. + * Create virtual displays, change their configurations and release them. The number of + * displays is set by the {@link #data()} parameter. */ private void testDisplayEventsInternal(boolean cached, boolean frozen) { Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + cached + " " + frozen); @@ -445,110 +380,6 @@ public class DisplayEventDeliveryTest { } /** - * Launch the test activity that would listen to display events. Return its process ID. - */ - private int launchTestActivity() { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY); - intent.putExtra(TEST_MESSENGER, mMessenger); - intent.putExtra(TEST_DISPLAYS, mDisplayCount); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - SystemUtil.runWithShellPermissionIdentity( - () -> { - mContext.startActivity(intent); - }, - android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); - waitLatch(mLatchActivityLaunch); - - try { - String cmd = "pidof " + TEST_PACKAGE; - String result = SystemUtil.runShellCommand(mInstrumentation, cmd); - return Integer.parseInt(result.trim()); - } catch (IOException e) { - fail("failed to get pid of test package"); - return 0; - } catch (NumberFormatException e) { - fail("failed to parse pid " + e); - return 0; - } - } - - /** - * Bring the test activity back to top - */ - private void bringTestActivityTop() { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - SystemUtil.runWithShellPermissionIdentity( - () -> { - mContext.startActivity(intent); - }, - android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); - } - - /** - * Bring the test activity into cached mode by launching another 2 apps - */ - private void makeTestActivityCached() { - // Launch another activity to bring the test activity into background - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClass(mContext, SimpleActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - - // Launch another activity to bring the test activity into cached mode - Intent intent2 = new Intent(Intent.ACTION_MAIN); - intent2.setClass(mContext, SimpleActivity2.class); - intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - SystemUtil.runWithShellPermissionIdentity( - () -> { - mInstrumentation.startActivitySync(intent); - mInstrumentation.startActivitySync(intent2); - }, - android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); - waitLatch(mLatchActivityCached); - } - - // Sleep, ignoring interrupts. - private void pause(int s) { - try { Thread.sleep(s * 1000); } catch (Exception e) { } - } - - /** - * Freeze the test activity. - */ - private void makeTestActivityFrozen(int pid) { - // The delay here is meant to allow pending binder transactions to drain. A process - // cannot be frozen if it has pending binder transactions, and attempting to freeze such a - // process more than a few times will result in the system killing the process. - pause(5); - try { - String cmd = "am freeze --sticky "; - SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE); - } catch (IOException e) { - fail(e.toString()); - } - // Wait for the freeze to complete in the kernel and for the frozen process - // notification to settle out. - waitForProcessFreeze(pid, 5 * 1000); - } - - /** - * Freeze the test activity. - */ - private void makeTestActivityUnfrozen(int pid) { - try { - String cmd = "am unfreeze --sticky "; - SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE); - } catch (IOException e) { - fail(e.toString()); - } - // Wait for the freeze to complete in the kernel and for the frozen process - // notification to settle out. - waitForProcessUnfreeze(pid, 5 * 1000); - } - - /** * Create a virtual display * * @param name The name of the new virtual display @@ -560,15 +391,4 @@ public class DisplayEventDeliveryTest { VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY /* flags: a public virtual display that another app can access */); } - - /** - * Wait for CountDownLatch with timeout - */ - private void waitLatch(CountDownLatch latch) { - try { - latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java new file mode 100644 index 000000000000..2911b9bb35c7 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; + +import static org.junit.Assert.fail; + +import android.app.ActivityManager; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.os.BinderProxy; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Messenger; +import android.platform.test.annotations.AppModeSdkSandbox; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.compatibility.common.util.SystemUtil; +import com.android.compatibility.common.util.TestUtils; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") +public abstract class EventDeliveryTestBase { + protected static final int MESSAGE_LAUNCHED = 1; + protected static final int MESSAGE_CALLBACK = 2; + + protected static final long EVENT_TIMEOUT_MSEC = 100; + protected static final long TEST_FAILURE_TIMEOUT_MSEC = 10000; + + private static final String TEST_MESSENGER = "MESSENGER"; + + private Instrumentation mInstrumentation; + private Context mContext; + protected DisplayManager mDisplayManager; + private ActivityManager mActivityManager; + private ActivityManager.OnUidImportanceListener mUidImportanceListener; + protected CountDownLatch mLatchActivityLaunch; + private CountDownLatch mLatchActivityCached; + private HandlerThread mHandlerThread; + private Handler mHandler; + private Messenger mMessenger; + protected int mPid; + protected int mUid; + + protected abstract String getTag(); + + protected abstract Handler getHandler(Looper looper); + + protected abstract String getTestPackage(); + + protected abstract String getTestActivity(); + + protected abstract void putExtra(Intent intent); + + protected void setUp() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mContext = mInstrumentation.getContext(); + mDisplayManager = mContext.getSystemService(DisplayManager.class); + mLatchActivityLaunch = new CountDownLatch(1); + mLatchActivityCached = new CountDownLatch(1); + mActivityManager = mContext.getSystemService(ActivityManager.class); + mUidImportanceListener = (uid, importance) -> { + if (uid == mUid && importance == IMPORTANCE_CACHED) { + Log.d(getTag(), "Listener " + uid + " becomes " + importance); + mLatchActivityCached.countDown(); + } + }; + SystemUtil.runWithShellPermissionIdentity(() -> + mActivityManager.addOnUidImportanceListener(mUidImportanceListener, + IMPORTANCE_CACHED)); + mHandlerThread = new HandlerThread("handler"); + mHandlerThread.start(); + mHandler = getHandler(mHandlerThread.getLooper()); + mMessenger = new Messenger(mHandler); + mPid = 0; + } + + protected void tearDown() throws Exception { + mActivityManager.removeOnUidImportanceListener(mUidImportanceListener); + mHandlerThread.quitSafely(); + SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + getTestPackage()); + } + + /** + * Return true if the freezer is enabled on this platform and if freezer notifications are + * supported. It is not enough to test that the freezer notification feature is enabled + * because some devices do not have the necessary kernel support. + */ + protected boolean isAppFreezerEnabled() { + try { + return ActivityManager.getService().isAppFreezerEnabled() + && android.os.Flags.binderFrozenStateChangeCallback() + && BinderProxy.isFrozenStateChangeCallbackSupported(); + } catch (Exception e) { + Log.e(getTag(), "isAppFreezerEnabled() failed: " + e); + return false; + } + } + + private void waitForProcessFreeze(int pid, long timeoutMs) { + // TODO: Add a listener to monitor freezer state changes. + SystemUtil.runWithShellPermissionIdentity(() -> { + TestUtils.waitUntil( + "Timed out waiting for test process to be frozen; pid=" + pid, + (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), + () -> mActivityManager.isProcessFrozen(pid)); + }); + } + + private void waitForProcessUnfreeze(int pid, long timeoutMs) { + // TODO: Add a listener to monitor freezer state changes. + SystemUtil.runWithShellPermissionIdentity(() -> { + TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid, + (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), + () -> !mActivityManager.isProcessFrozen(pid)); + }); + } + + /** + * Launch the test activity that would listen to events. Return its process ID. + */ + protected int launchTestActivity() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(getTestPackage(), getTestActivity()); + intent.putExtra(TEST_MESSENGER, mMessenger); + putExtra(intent); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + SystemUtil.runWithShellPermissionIdentity( + () -> { + mContext.startActivity(intent); + }, + android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); + waitLatch(mLatchActivityLaunch); + + try { + String cmd = "pidof " + getTestPackage(); + String result = SystemUtil.runShellCommand(mInstrumentation, cmd); + return Integer.parseInt(result.trim()); + } catch (IOException e) { + fail("failed to get pid of test package"); + return 0; + } catch (NumberFormatException e) { + fail("failed to parse pid " + e); + return 0; + } + } + + /** + * Bring the test activity back to top + */ + protected void bringTestActivityTop() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(getTestPackage(), getTestActivity()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + SystemUtil.runWithShellPermissionIdentity( + () -> { + mContext.startActivity(intent); + }, + android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); + } + + + /** + * Bring the test activity into cached mode by launching another 2 apps + */ + protected void makeTestActivityCached() { + // Launch another activity to bring the test activity into background + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(mContext, SimpleActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + + // Launch another activity to bring the test activity into cached mode + Intent intent2 = new Intent(Intent.ACTION_MAIN); + intent2.setClass(mContext, SimpleActivity2.class); + intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + SystemUtil.runWithShellPermissionIdentity( + () -> { + mInstrumentation.startActivitySync(intent); + mInstrumentation.startActivitySync(intent2); + }, + android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); + waitLatch(mLatchActivityCached); + } + + // Sleep, ignoring interrupts. + private void pause(int s) { + try { + Thread.sleep(s * 1000L); + } catch (Exception ignored) { } + } + + /** + * Freeze the test activity. + */ + protected void makeTestActivityFrozen(int pid) { + // The delay here is meant to allow pending binder transactions to drain. A process + // cannot be frozen if it has pending binder transactions, and attempting to freeze such a + // process more than a few times will result in the system killing the process. + pause(5); + try { + String cmd = "am freeze --sticky "; + SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage()); + } catch (IOException e) { + fail(e.toString()); + } + // Wait for the freeze to complete in the kernel and for the frozen process + // notification to settle out. + waitForProcessFreeze(pid, 5 * 1000); + } + + /** + * Freeze the test activity. + */ + protected void makeTestActivityUnfrozen(int pid) { + try { + String cmd = "am unfreeze --sticky "; + SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage()); + } catch (IOException e) { + fail(e.toString()); + } + // Wait for the freeze to complete in the kernel and for the frozen process + // notification to settle out. + waitForProcessUnfreeze(pid, 5 * 1000); + } + + /** + * Wait for CountDownLatch with timeout + */ + private void waitLatch(CountDownLatch latch) { + try { + latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java new file mode 100644 index 000000000000..5fd248dba53f --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.content.Intent; +import android.hardware.display.DisplayTopology; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Tests that applications can receive topology updates correctly. + */ +public class TopologyUpdateDeliveryTest extends EventDeliveryTestBase { + private static final String TAG = TopologyUpdateDeliveryTest.class.getSimpleName(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final String TEST_PACKAGE = "com.android.servicestests.apps.topologytestapp"; + private static final String TEST_ACTIVITY = TEST_PACKAGE + ".TopologyUpdateActivity"; + + // Topology updates we expect to receive before timeout + private final LinkedBlockingQueue<DisplayTopology> mExpectations = new LinkedBlockingQueue<>(); + + /** + * Add the received topology update from the test activity to the queue + * + * @param topology The corresponding topology update + */ + private void addTopologyUpdate(DisplayTopology topology) { + Log.d(TAG, "Received " + topology); + mExpectations.offer(topology); + } + + /** + * Assert that there isn't any unexpected display event from the test activity + */ + private void assertNoTopologyUpdates() { + try { + assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Wait for the expected topology update from the test activity + * + * @param expect The expected topology update + */ + private void waitTopologyUpdate(DisplayTopology expect) { + while (true) { + try { + DisplayTopology update = mExpectations.poll(TEST_FAILURE_TIMEOUT_MSEC, + TimeUnit.MILLISECONDS); + assertNotNull(update); + if (expect.equals(update)) { + Log.d(TAG, "Found " + update); + return; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private class TestHandler extends Handler { + TestHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(@NonNull Message msg) { + switch (msg.what) { + case MESSAGE_LAUNCHED: + mPid = msg.arg1; + mUid = msg.arg2; + Log.d(TAG, "Launched " + mPid + " " + mUid); + mLatchActivityLaunch.countDown(); + break; + case MESSAGE_CALLBACK: + DisplayTopology topology = (DisplayTopology) msg.obj; + Log.d(TAG, "Callback " + topology); + addTopologyUpdate(topology); + break; + default: + fail("Unexpected value: " + msg.what); + break; + } + } + } + + @Before + public void setUp() { + super.setUp(); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + } + + @Override + protected String getTag() { + return TAG; + } + + @Override + protected Handler getHandler(Looper looper) { + return new TestHandler(looper); + } + + @Override + protected String getTestPackage() { + return TEST_PACKAGE; + } + + @Override + protected String getTestActivity() { + return TEST_ACTIVITY; + } + + @Override + protected void putExtra(Intent intent) { } + + private void testTopologyUpdateInternal(boolean cached, boolean frozen) { + Log.d(TAG, "Start test testTopologyUpdate " + cached + " " + frozen); + // Launch activity and start listening to topology updates + int pid = launchTestActivity(); + + // The test activity in cached or frozen mode won't receive the pending topology updates. + if (cached) { + makeTestActivityCached(); + } + if (frozen) { + makeTestActivityFrozen(pid); + } + + // Change the topology + int primaryDisplayId = 3; + DisplayTopology.TreeNode root = new DisplayTopology.TreeNode(primaryDisplayId, + /* width= */ 600, /* height= */ 400, DisplayTopology.TreeNode.POSITION_LEFT, + /* offset= */ 0); + DisplayTopology.TreeNode child = new DisplayTopology.TreeNode(/* displayId= */ 1, + /* width= */ 800, /* height= */ 600, DisplayTopology.TreeNode.POSITION_LEFT, + /* offset= */ 0); + root.addChild(child); + DisplayTopology topology = new DisplayTopology(root, primaryDisplayId); + mDisplayManager.setDisplayTopology(topology); + + if (cached || frozen) { + assertNoTopologyUpdates(); + } else { + waitTopologyUpdate(topology); + } + + // Unfreeze the test activity, if it was frozen. + if (frozen) { + makeTestActivityUnfrozen(pid); + } + + if (cached || frozen) { + // Always ensure the test activity is not cached. + bringTestActivityTop(); + + // The test activity becomes non-cached and should receive the pending topology updates + waitTopologyUpdate(topology); + } + } + + @Test + @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY) + public void testTopologyUpdate() { + testTopologyUpdateInternal(false, false); + } + + /** + * The app is moved to cached and the test verifies that no updates are delivered to the cached + * app. + */ + @Test + @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY) + public void testTopologyUpdateCached() { + testTopologyUpdateInternal(true, false); + } + + /** + * The app is frozen and the test verifies that no updates are delivered to the frozen app. + */ + @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN, + com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY}) + @Test + public void testTopologyUpdateFrozen() { + assumeTrue(isAppFreezerEnabled()); + testTopologyUpdateInternal(false, true); + } + + /** + * The app is cached and frozen and the test verifies that no updates are delivered to the app. + */ + @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN, + com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY}) + @Test + public void testTopologyUpdateCachedFrozen() { + assumeTrue(isAppFreezerEnabled()); + testTopologyUpdateInternal(true, true); + } +} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index d702cae248a9..067fba9893e5 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -31,6 +31,7 @@ android_test { "test-apps/SuspendTestApp/src/**/*.java", "test-apps/DisplayManagerTestApp/src/**/*.java", + "test-apps/TopologyTestApp/src/**/*.java", ], static_libs: [ @@ -141,6 +142,7 @@ android_test { data: [ ":DisplayManagerTestApp", + ":TopologyTestApp", ":SimpleServiceTestApp1", ":SimpleServiceTestApp2", ":SimpleServiceTestApp3", diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml index 5298251b79f7..9a4983482522 100644 --- a/services/tests/servicestests/AndroidTest.xml +++ b/services/tests/servicestests/AndroidTest.xml @@ -36,6 +36,7 @@ <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> <option name="test-file-name" value="DisplayManagerTestApp.apk" /> + <option name="test-file-name" value="TopologyTestApp.apk" /> <option name="test-file-name" value="FrameworksServicesTests.apk" /> <option name="test-file-name" value="SuspendTestApp.apk" /> <option name="test-file-name" value="SimpleServiceTestApp1.apk" /> diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index 99c922ca30c4..df77866b5e7f 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -25,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -866,6 +867,23 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void scrollPanelController_directionalButtonsHideIndicator() { + injectFakeMouseActionHoverMoveEvent(); + + // Create a spy on the real object to verify method calls. + AutoclickIndicatorView spyIndicatorView = spy(mController.mAutoclickIndicatorView); + mController.mAutoclickIndicatorView = spyIndicatorView; + + // Simulate hover on direction button. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_UP, true); + + // Verify clearIndicator was called. + verify(spyIndicatorView).clearIndicator(); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() { MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); mController.setNext(motionEventCaptor); diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp new file mode 100644 index 000000000000..dcf9cc216687 --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp @@ -0,0 +1,38 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test_helper_app { + name: "TopologyTestApp", + + srcs: ["**/*.java"], + + dex_preopt: { + enabled: false, + }, + optimize: { + enabled: false, + }, + + platform_apis: true, + certificate: "platform", +} diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml new file mode 100644 index 000000000000..dad2315148df --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.servicestests.apps.topologytestapp"> + + <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> + + <application android:label="TopologyUpdateTestApp"> + <activity android:name="com.android.servicestests.apps.topologytestapp.TopologyUpdateActivity" + android:exported="true" /> + </application> + +</manifest> diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS new file mode 100644 index 000000000000..e9557f84f8fb --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 345010 + +include /services/core/java/com/android/server/display/OWNERS diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java new file mode 100644 index 000000000000..b35ba3c2c60c --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.servicestests.apps.topologytestapp; + +import android.app.Activity; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; + +import java.util.function.Consumer; + +/** + * A simple activity listening to topology updates + */ +public class TopologyUpdateActivity extends Activity { + public static final int MESSAGE_LAUNCHED = 1; + public static final int MESSAGE_CALLBACK = 2; + + private static final String TAG = TopologyUpdateActivity.class.getSimpleName(); + + private static final String TEST_MESSENGER = "MESSENGER"; + + private Messenger mMessenger; + private DisplayManager mDisplayManager; + private final Consumer<DisplayTopology> mTopologyListener = this::callback; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + mMessenger = intent.getParcelableExtra(TEST_MESSENGER, Messenger.class); + mDisplayManager = getApplicationContext().getSystemService(DisplayManager.class); + mDisplayManager.registerTopologyListener(getMainExecutor(), mTopologyListener); + launched(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mDisplayManager.unregisterTopologyListener(mTopologyListener); + } + + private void launched() { + try { + Message msg = Message.obtain(); + msg.what = MESSAGE_LAUNCHED; + msg.arg1 = android.os.Process.myPid(); + msg.arg2 = Process.myUid(); + Log.d(TAG, "Launched"); + mMessenger.send(msg); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + private void callback(DisplayTopology topology) { + try { + Message msg = Message.obtain(); + msg.what = MESSAGE_CALLBACK; + msg.obj = topology; + Log.d(TAG, "Msg " + topology); + mMessenger.send(msg); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 68229688c238..f3d5e39ec127 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -35,6 +35,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.policy.PhoneWindowManager.EXTRA_TRIGGER_HUB; +import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP; import static com.google.common.truth.Truth.assertThat; @@ -64,6 +65,7 @@ import android.view.contentprotection.flags.Flags; import androidx.test.filters.SmallTest; import com.android.internal.util.test.LocalServiceKeeperRule; +import com.android.internal.widget.LockPatternUtils; import com.android.server.input.InputManagerInternal; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.keyguard.KeyguardServiceDelegate; @@ -120,6 +122,8 @@ public class PhoneWindowManagerTests { private DisplayPolicy mDisplayPolicy; @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate; + @Mock + private LockPatternUtils mLockPatternUtils; @Before public void setUp() { @@ -254,6 +258,7 @@ public class PhoneWindowManagerTests { @Test public void powerPress_hubOrDreamOrSleep_goesToSleepFromDream() { when(mDisplayPolicy.isAwake()).thenReturn(true); + when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false); initPhoneWindowManager(); // Set power button behavior. @@ -275,6 +280,7 @@ public class PhoneWindowManagerTests { @Test public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() { when(mDisplayPolicy.isAwake()).thenReturn(true); + when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false); mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER, PERMISSION_GRANTED); initPhoneWindowManager(); @@ -303,6 +309,7 @@ public class PhoneWindowManagerTests { @Test public void powerPress_hubOrDreamOrSleep_hubNotAvailableDreams() { when(mDisplayPolicy.isAwake()).thenReturn(true); + when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false); initPhoneWindowManager(); // Set power button behavior. @@ -323,6 +330,77 @@ public class PhoneWindowManagerTests { verify(mDreamManagerInternal).requestDream(); } + @Test + public void powerPress_dreamOrAwakeOrSleep_awakeFromDream() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + initPhoneWindowManager(); + + // Set power button behavior. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.POWER_BUTTON_SHORT_PRESS, + SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP); + mPhoneWindowManager.updateSettings(null); + + // Can not dream when device is dreaming. + when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false); + // Device is dreaming. + when(mDreamManagerInternal.isDreaming()).thenReturn(true); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // Dream is stopped. + verify(mDreamManagerInternal) + .stopDream(false /*immediate*/, "short press power" /*reason*/); + } + + @Test + public void powerPress_dreamOrAwakeOrSleep_canNotDreamGoToSleep() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + initPhoneWindowManager(); + + // Set power button behavior. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.POWER_BUTTON_SHORT_PRESS, + SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP); + mPhoneWindowManager.updateSettings(null); + + // Can not dream for other reasons. + when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false); + // Device is not dreaming. + when(mDreamManagerInternal.isDreaming()).thenReturn(false); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // Device goes to sleep. + verify(mPowerManager).goToSleep(eventTime, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0); + } + + @Test + public void powerPress_dreamOrAwakeOrSleep_dreamFromActive() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + initPhoneWindowManager(); + + // Set power button behavior. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.POWER_BUTTON_SHORT_PRESS, + SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP); + mPhoneWindowManager.updateSettings(null); + + // Can dream when active. + when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(true); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // Dream is requested. + verify(mDreamManagerInternal).requestDream(); + } + private void initPhoneWindowManager() { mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy; mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); @@ -346,6 +424,11 @@ public class PhoneWindowManagerTests { return mKeyguardServiceDelegate; } + @Override + LockPatternUtils getLockPatternUtils() { + return mLockPatternUtils; + } + /** * {@code WindowWakeUpPolicy} registers a local service in its constructor, easier to just * mock it out so we don't have to unregister it after every test. diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 9452e588953e..5576ec0b882f 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -38,14 +38,14 @@ class ConvertCommand : public Command { "--enable-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", + "Only applies sparse encoding if minSdk of the APK is >= 32", &enable_sparse_encoding_); - AddOptionalSwitch("--force-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Applies sparse encoding to all resources regardless of minSdk.", - &force_sparse_encoding_); + AddOptionalSwitch( + "--force-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set", + &force_sparse_encoding_); AddOptionalSwitch( "--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 977978834fcd..54a8c8625eb5 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -164,9 +164,12 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch("--enable-sparse-encoding", - "This decreases APK size at the cost of resource retrieval performance.", - &options_.use_sparse_encoding); + AddOptionalSwitch( + "--enable-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding if minSdk of the APK is >= 32", + &options_.use_sparse_encoding); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 012b0f230ca2..a8f547e3d96c 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -108,14 +108,14 @@ class OptimizeCommand : public Command { "--enable-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", + "Only applies sparse encoding if minSdk of the APK is >= 32", &options_.enable_sparse_encoding); - AddOptionalSwitch("--force-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Applies sparse encoding to all resources regardless of minSdk.", - &options_.force_sparse_encoding); + AddOptionalSwitch( + "--force-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set", + &options_.force_sparse_encoding); AddOptionalSwitch( "--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 50144ae816b6..d19c9f2d5d75 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -197,13 +197,16 @@ class PackageFlattener { bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled || sparse_entries_ == SparseEntriesMode::Forced; - if (sparse_entries_ == SparseEntriesMode::Forced || - (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { - // Sparse encode if forced or sdk version is not set in context and config. - } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. - sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); - } + // Only sparse encode if the entries will be read on platforms S_V2+. Sparse encoding + // is not supported on older platforms (b/197642721, b/197976367). + // + // We also allow sparse encoding for minSdk is 0 (not set) if sparse encoding is forced, + // in order to support Bundletool's usage of aapt2 where minSdk is not set in splits. + bool meets_min_sdk_requirement_for_sparse_encoding = + (context_->GetMinSdkVersion() >= SDK_S_V2) || + (context_->GetMinSdkVersion() == 0 && sparse_entries_ == SparseEntriesMode::Forced); + + sparse_encode = sparse_encode && meets_min_sdk_requirement_for_sparse_encoding; // Only sparse encode if the offsets are representable in 2 bytes. sparse_encode = sparse_encode && short_offsets; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 9156b96b67ec..0e8aae14a350 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -15,6 +15,7 @@ */ #include "format/binary/TableFlattener.h" +#include <string> #include "android-base/stringprintf.h" #include "androidfw/TypeWrappers.h" @@ -326,6 +327,28 @@ static std::unique_ptr<ResourceTable> BuildTableWithSparseEntries( return table; } +static void CheckSparseEntries(IAaptContext* context, const ConfigDescription& sparse_config, + const std::string& sparse_contents) { + ResourceTable sparse_table; + BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"), + sparse_contents.data(), sparse_contents.size()); + ASSERT_TRUE(parser.Parse()); + + auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0", + sparse_config); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(0u, value->value.data); + + ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1", + sparse_config), + IsNull()); + + value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4", + sparse_config); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(4u, value->value.data); +} + TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { std::unique_ptr<IAaptContext> context = test::ContextBuilder() .SetCompilationPackage("android") @@ -347,29 +370,56 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); - // Attempt to parse the sparse contents. + CheckSparseEntries(context.get(), sparse_config, sparse_contents); +} - ResourceTable sparse_table; - BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"), - sparse_contents.data(), sparse_contents.size()); - ASSERT_TRUE(parser.Parse()); +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2AndForced) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .SetCompilationPackage("android") + .SetPackageId(0x01) + .SetMinSdkVersion(SDK_S_V2) + .Build(); - auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(0u, value->value.data); + const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB"); + auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); - ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1", - sparse_config), - IsNull()); + TableFlattenerOptions options; + options.sparse_entries = SparseEntriesMode::Forced; - value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(4u, value->value.data); + std::string no_sparse_contents; + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + + std::string sparse_contents; + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + + EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + + CheckSparseEntries(context.get(), sparse_config, sparse_contents); } -TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) { +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .SetCompilationPackage("android") + .SetPackageId(0x01) + .SetMinSdkVersion(SDK_LOLLIPOP) + .Build(); + + const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB"); + auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); + + TableFlattenerOptions options; + options.sparse_entries = SparseEntriesMode::Enabled; + + std::string no_sparse_contents; + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + + std::string sparse_contents; + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + + EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); +} + +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndConfigSdkVersionSV2) { std::unique_ptr<IAaptContext> context = test::ContextBuilder() .SetCompilationPackage("android") .SetPackageId(0x01) @@ -391,7 +441,7 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) { EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); } -TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) { +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndForced) { std::unique_ptr<IAaptContext> context = test::ContextBuilder() .SetCompilationPackage("android") .SetPackageId(0x01) @@ -410,7 +460,7 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) { std::string sparse_contents; ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); - EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); } TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { @@ -429,28 +479,28 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { std::string sparse_contents; ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); - EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); +} - // Attempt to parse the sparse contents. +TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSetAndForced) { + std::unique_ptr<IAaptContext> context = + test::ContextBuilder().SetCompilationPackage("android").SetPackageId(0x01).Build(); - ResourceTable sparse_table; - BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"), - sparse_contents.data(), sparse_contents.size()); - ASSERT_TRUE(parser.Parse()); + const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB"); + auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); - auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(0u, value->value.data); + TableFlattenerOptions options; + options.sparse_entries = SparseEntriesMode::Forced; - ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1", - sparse_config), - IsNull()); + std::string no_sparse_contents; + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); - value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(4u, value->value.data); + std::string sparse_contents; + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + + EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + + CheckSparseEntries(context.get(), sparse_config, sparse_contents); } TEST_F(TableFlattenerTest, DoNotUseSparseEntryForDenseConfig) { diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 6bdbaaed9858..413f817ea8fd 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -5,6 +5,10 @@ 2017. This README will be updated more frequently in the future. - Added a new flag `--no-compress-fonts`. This can significantly speed up loading fonts from APK assets, at the cost of increasing the storage size of the APK. +- Changed the behavior of `--enable-sparse-encoding`. Sparse encoding is only applied if the + minSdkVersion is >= 32. +- Changed the behavior of `--force-sparse-encoding`. Sparse encoding is only applied if the + minSdkVersion is >= 32 or is not set. ## Version 2.19 - Added navigation resource type. |