diff options
73 files changed, 2320 insertions, 1227 deletions
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index b0332c32bfbc..ffb79b3fc552 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -1642,6 +1642,7 @@ public class NotificationManager { * * <p> */ + // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public Policy getNotificationPolicy() { INotificationManager service = getService(); try { @@ -1660,6 +1661,7 @@ public class NotificationManager { * * @param policy The new desired policy. */ + // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public void setNotificationPolicy(@NonNull Policy policy) { checkRequired("policy", policy); INotificationManager service = getService(); @@ -2608,6 +2610,7 @@ public class NotificationManager { * Only available if policy access is granted to this package. See * {@link #isNotificationPolicyAccessGranted}. */ + // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) { final INotificationManager service = getService(); try { diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index ff4dfc7cc079..305b751f0cef 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1004,6 +1004,8 @@ public class ZenModeConfig implements Parcelable { priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS; conversationSenders = getConversationSendersWithDefault( zenPolicy.getPriorityConversationSenders(), conversationSenders); + } else { + conversationSenders = CONVERSATION_SENDERS_NONE; } if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CALLS, @@ -1102,7 +1104,7 @@ public class ZenModeConfig implements Parcelable { return (policy.suppressedVisualEffects & visualEffect) == 0; } - private int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders, + private static int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders, int defaultPolicySender) { switch (senders) { case ZenPolicy.PEOPLE_TYPE_ANYONE: @@ -1116,7 +1118,7 @@ public class ZenModeConfig implements Parcelable { } } - private int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders, + private static int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders, int defaultPolicySender) { switch (senders) { case ZenPolicy.CONVERSATION_SENDERS_ANYONE: diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig new file mode 100644 index 000000000000..b63a969337e0 --- /dev/null +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -0,0 +1,9 @@ +package: "com.android.window.flags" + +flag { + name: "allows_screen_size_decoupled_from_status_bar_and_cutout" + namespace: "large_screen_experiences_app_compat" + description: "When necessary, configuration decoupled from status bar and display cutout" + bug: "291870756" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 15c188d2140b..cec83dee9c3a 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5245,6 +5245,11 @@ <!-- Zen mode - name of default automatic calendar time-based rule that is triggered every night (when sleeping). [CHAR LIMIT=40] --> <string name="zen_mode_default_every_night_name">Sleeping</string> + <!-- Zen mode - Condition summary when a rule is activated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] --> + <string name="zen_mode_implicit_activated">On</string> + <!-- Zen mode - Condition summary when a rule is deactivated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] --> + <string name="zen_mode_implicit_deactivated">Off</string> + <!-- Indication that the current volume and other effects (vibration) are being suppressed by a third party, such as a notification listener. [CHAR LIMIT=30] --> <string name="muted_by"><xliff:g id="third_party">%1$s</xliff:g> is muting some sounds</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e19d5486768a..16ad5c909575 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2574,6 +2574,8 @@ <java-symbol type="string" name="zen_mode_default_weekends_name" /> <java-symbol type="string" name="zen_mode_default_events_name" /> <java-symbol type="string" name="zen_mode_default_every_night_name" /> + <java-symbol type="string" name="zen_mode_implicit_activated" /> + <java-symbol type="string" name="zen_mode_implicit_deactivated" /> <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" /> <java-symbol type="string" name="display_rotation_camera_compat_toast_in_multi_window" /> <java-symbol type="array" name="config_system_condition_providers" /> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index d5b29e384c09..d5fab441cd46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -822,14 +822,23 @@ public class PipTransition extends PipTransitionController { + "participant"); } - // Make sure other open changes are visible as entering PIP. Some may be hidden in - // Transitions#setupStartState because the transition type is OPEN (such as auto-enter). + // Make sure other non-pip changes are handled correctly. for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change == enterPip) continue; if (TransitionUtil.isOpeningType(change.getMode())) { + // For other open changes that are visible when entering PIP, some may be hidden in + // Transitions#setupStartState because the transition type is OPEN (such as + // auto-enter). final SurfaceControl leash = change.getLeash(); startTransaction.show(leash).setAlpha(leash, 1.f); + } else if (TransitionUtil.isClosingType(change.getMode())) { + // For other close changes that are invisible as entering PIP, hide them immediately + // to avoid showing a freezing surface. + // Ideally, we should let other handler to handle them (likely RemoteHandler by + // Launcher). + final SurfaceControl leash = change.getLeash(); + startTransaction.hide(leash); } } diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index e706eb083ab6..d55d28d469d0 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -527,52 +527,65 @@ Frame VulkanManager::dequeueNextBuffer(VulkanSurface* surface) { return Frame(surface->logicalWidth(), surface->logicalHeight(), bufferAge); } -struct DestroySemaphoreInfo { +class SharedSemaphoreInfo : public LightRefBase<SharedSemaphoreInfo> { PFN_vkDestroySemaphore mDestroyFunction; VkDevice mDevice; VkSemaphore mSemaphore; + GrBackendSemaphore mGrBackendSemaphore; - DestroySemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device, - VkSemaphore semaphore) - : mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) {} + SharedSemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device, + VkSemaphore semaphore) + : mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) { + mGrBackendSemaphore.initVulkan(semaphore); + } + + ~SharedSemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); } + + friend class LightRefBase<SharedSemaphoreInfo>; + friend class sp<SharedSemaphoreInfo>; + +public: + VkSemaphore semaphore() const { return mSemaphore; } - ~DestroySemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); } + GrBackendSemaphore* grBackendSemaphore() { return &mGrBackendSemaphore; } }; static void destroy_semaphore(void* context) { - DestroySemaphoreInfo* info = reinterpret_cast<DestroySemaphoreInfo*>(context); - delete info; + SharedSemaphoreInfo* info = reinterpret_cast<SharedSemaphoreInfo*>(context); + info->decStrong(0); } VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { ATRACE_NAME("Vulkan finish frame"); - VkExportSemaphoreCreateInfo exportInfo; - exportInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO; - exportInfo.pNext = nullptr; - exportInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT; - - VkSemaphoreCreateInfo semaphoreInfo; - semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - semaphoreInfo.pNext = &exportInfo; - semaphoreInfo.flags = 0; - VkSemaphore semaphore; - VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore); - ALOGE_IF(VK_SUCCESS != err, "VulkanManager::makeSwapSemaphore(): Failed to create semaphore"); - - GrBackendSemaphore backendSemaphore; - backendSemaphore.initVulkan(semaphore); - + sp<SharedSemaphoreInfo> sharedSemaphore; GrFlushInfo flushInfo; - if (err == VK_SUCCESS) { - flushInfo.fNumSemaphores = 1; - flushInfo.fSignalSemaphores = &backendSemaphore; - flushInfo.fFinishedProc = destroy_semaphore; - flushInfo.fFinishedContext = - new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore); - } else { - semaphore = VK_NULL_HANDLE; + + { + VkExportSemaphoreCreateInfo exportInfo; + exportInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO; + exportInfo.pNext = nullptr; + exportInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT; + + VkSemaphoreCreateInfo semaphoreInfo; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semaphoreInfo.pNext = &exportInfo; + semaphoreInfo.flags = 0; + VkSemaphore semaphore; + VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore); + ALOGE_IF(VK_SUCCESS != err, + "VulkanManager::makeSwapSemaphore(): Failed to create semaphore"); + + if (err == VK_SUCCESS) { + sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore); + flushInfo.fNumSemaphores = 1; + flushInfo.fSignalSemaphores = sharedSemaphore->grBackendSemaphore(); + flushInfo.fFinishedProc = destroy_semaphore; + sharedSemaphore->incStrong(0); + flushInfo.fFinishedContext = sharedSemaphore.get(); + } } + GrDirectContext* context = GrAsDirectContext(surface->recordingContext()); ALOGE_IF(!context, "Surface is not backed by gpu"); GrSemaphoresSubmitted submitted = context->flush( @@ -581,37 +594,34 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { VkDrawResult drawResult{ .submissionTime = systemTime(), }; - if (semaphore != VK_NULL_HANDLE) { - if (submitted == GrSemaphoresSubmitted::kYes) { - if (mFrameBoundaryANDROID) { - // retrieve VkImage used as render target - VkImage image = VK_NULL_HANDLE; - GrBackendRenderTarget backendRenderTarget = - SkSurfaces::GetBackendRenderTarget( - surface, SkSurfaces::BackendHandleAccess::kFlushRead); - if (backendRenderTarget.isValid()) { - GrVkImageInfo info; - if (GrBackendRenderTargets::GetVkImageInfo(backendRenderTarget, &info)) { - image = info.fImage; - } else { - ALOGE("Frame boundary: backend is not vulkan"); - } + if (sharedSemaphore) { + if (submitted == GrSemaphoresSubmitted::kYes && mFrameBoundaryANDROID) { + // retrieve VkImage used as render target + VkImage image = VK_NULL_HANDLE; + GrBackendRenderTarget backendRenderTarget = SkSurfaces::GetBackendRenderTarget( + surface, SkSurfaces::BackendHandleAccess::kFlushRead); + if (backendRenderTarget.isValid()) { + GrVkImageInfo info; + if (GrBackendRenderTargets::GetVkImageInfo(backendRenderTarget, &info)) { + image = info.fImage; } else { - ALOGE("Frame boundary: invalid backend render target"); + ALOGE("Frame boundary: backend is not vulkan"); } - // frameBoundaryANDROID needs to know about mSwapSemaphore, but - // it won't wait on it. - mFrameBoundaryANDROID(mDevice, semaphore, image); + } else { + ALOGE("Frame boundary: invalid backend render target"); } + // frameBoundaryANDROID needs to know about mSwapSemaphore, but + // it won't wait on it. + mFrameBoundaryANDROID(mDevice, sharedSemaphore->semaphore(), image); } VkSemaphoreGetFdInfoKHR getFdInfo; getFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; getFdInfo.pNext = nullptr; - getFdInfo.semaphore = semaphore; + getFdInfo.semaphore = sharedSemaphore->semaphore(); getFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT; int fenceFd = -1; - err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd); + VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd); ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd"); drawResult.presentFence.reset(fenceFd); } else { @@ -732,15 +742,15 @@ status_t VulkanManager::createReleaseFence(int* nativeFence, GrDirectContext* gr return INVALID_OPERATION; } - GrBackendSemaphore backendSemaphore; - backendSemaphore.initVulkan(semaphore); + auto sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore); // Even if Skia fails to submit the semaphore, it will still call the destroy_semaphore callback GrFlushInfo flushInfo; flushInfo.fNumSemaphores = 1; - flushInfo.fSignalSemaphores = &backendSemaphore; + flushInfo.fSignalSemaphores = sharedSemaphore->grBackendSemaphore(); flushInfo.fFinishedProc = destroy_semaphore; - flushInfo.fFinishedContext = new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore); + sharedSemaphore->incStrong(0); + flushInfo.fFinishedContext = sharedSemaphore.get(); GrSemaphoresSubmitted submitted = grContext->flush(flushInfo); grContext->submit(); diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java index 5a274353f68e..2b349d498d59 100644 --- a/media/java/android/media/AudioDeviceAttributes.java +++ b/media/java/android/media/AudioDeviceAttributes.java @@ -325,7 +325,7 @@ public final class AudioDeviceAttributes implements Parcelable { + " role:" + roleToString(mRole) + " type:" + (mRole == ROLE_OUTPUT ? AudioSystem.getOutputDeviceName(mNativeType) : AudioSystem.getInputDeviceName(mNativeType)) - + " addr:" + mAddress + + " addr:" + Utils.anonymizeBluetoothAddress(mNativeType, mAddress) + " name:" + mName + " profiles:" + mAudioProfiles.toString() + " descriptors:" + mAudioDescriptors.toString()); diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java index 9211c53a17bc..73bc6f96bf8b 100644 --- a/media/java/android/media/AudioDevicePort.java +++ b/media/java/android/media/AudioDevicePort.java @@ -156,7 +156,7 @@ public class AudioDevicePort extends AudioPort { AudioSystem.getOutputDeviceName(mType)); return "{" + super.toString() + ", mType: " + type - + ", mAddress: " + mAddress + + ", mAddress: " + Utils.anonymizeBluetoothAddress(mType, mAddress) + "}"; } } diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java index ecb6b3d495ad..d07f6118f6f4 100644 --- a/media/java/android/media/Utils.java +++ b/media/java/android/media/Utils.java @@ -657,4 +657,35 @@ public class Utils { // on the fly. private final boolean mForceRemoveConsistency; // default false } + + /** + * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app + * Must match the implementation of BluetoothUtils.toAnonymizedAddress() + * @param address MAC address to be anonymized + * @return anonymized MAC address + */ + public static @Nullable String anonymizeBluetoothAddress(@Nullable String address) { + if (address == null) { + return null; + } + if (address.length() != "AA:BB:CC:DD:EE:FF".length()) { + return address; + } + return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length()); + } + + /** + * Convert a Bluetooth MAC address to an anonymized one if the internal device type corresponds + * to a Bluetooth. + * @param deviceType the internal type of the audio device + * @param address MAC address to be anonymized + * @return anonymized MAC address + */ + public static @Nullable String anonymizeBluetoothAddress( + int deviceType, @Nullable String address) { + if (!AudioSystem.isBluetoothDevice(deviceType)) { + return address; + } + return anonymizeBluetoothAddress(address); + } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt index 1c92696efd29..223e99e61204 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,19 +57,24 @@ fun UserProfilePager(content: @Composable (userGroup: UserGroup) -> Unit) { private fun UserManager.getUserGroups(): List<UserGroup> { val userGroupList = mutableListOf<UserGroup>() - val profileToShowInSettings = getProfiles(UserHandle.myUserId()) - .map { userInfo -> userInfo to getUserProperties(userInfo.userHandle) } + val showInSettingsMap = getProfiles(UserHandle.myUserId()).groupBy { showInSettings(it) } - profileToShowInSettings - .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_WITH_PARENT } - .takeIf { it.isNotEmpty() } - ?.map { it.first } - ?.let { userInfos -> userGroupList += UserGroup(userInfos) } + showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_WITH_PARENT]?.let { + userGroupList += UserGroup(it) + } - profileToShowInSettings - .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_SEPARATE && - (!it.second.hideInSettingsInQuietMode || !it.first.isQuietModeEnabled) } - .forEach { userGroupList += UserGroup(userInfos = listOf(it.first)) } + showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_SEPARATE]?.forEach { + userGroupList += UserGroup(listOf(it)) + } return userGroupList } + +private fun UserManager.showInSettings(userInfo: UserInfo): Int { + val userProperties = getUserProperties(userInfo.userHandle) + return if (userInfo.isQuietModeEnabled && userProperties.hideInSettingsInQuietMode) { + UserProperties.SHOW_IN_SETTINGS_NO + } else { + userProperties.showInSettings + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt new file mode 100644 index 000000000000..e450364a9ab2 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.template.common + +import android.content.Context +import android.content.pm.UserInfo +import android.content.pm.UserProperties +import android.os.UserManager +import androidx.compose.material3.Text +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spaprivileged.framework.common.userManager +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy + +@RunWith(AndroidJUnit4::class) +class UserProfilePagerTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val mockUserManager = mock<UserManager> { + on { getProfiles(any()) } doReturn listOf(USER_0) + on { getUserProperties(USER_0.userHandle) } doReturn + UserProperties.Builder() + .setShowInSettings(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT) + .build() + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { userManager } doReturn mockUserManager + } + + @Test + fun userProfilePager() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + UserProfilePager { userGroup -> + Text(text = userGroup.userInfos.joinToString { it.id.toString() }) + } + } + } + + composeTestRule.onNodeWithText(USER_0.id.toString()).assertIsDisplayed() + } + + private companion object { + val USER_0 = UserInfo(0, "", 0) + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index aae61bd0f554..b77a60b5b5d6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -506,7 +506,10 @@ private inline fun <T> computeValue( // There is no ongoing transition. if (state !is TransitionState.Transition || state.fromScene == state.toScene) { - return idleValue + // Even if this element SceneTransitionLayout is not animated, the layout itself might be + // animated (e.g. by another parent SceneTransitionLayout), in which case this element still + // need to participate in the layout phase. + return currentValue() } // A transition was started but it's not ready yet (not all elements have been composed/laid diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt index 216608aff0cb..5d8eaf7f3d15 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt @@ -2,9 +2,10 @@ package com.android.compose.animation.scene import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.unit.IntSize interface DraggableHandler { - fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1) + fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int = 1) fun onDelta(pixels: Float) fun onDragStopped(velocity: Float) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index d0a5f5bfebc0..d48781a4529b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.addPointerInputChange import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity import androidx.compose.ui.util.fastForEach @@ -60,7 +61,7 @@ internal fun Modifier.multiPointerDraggable( orientation: Orientation, enabled: Boolean, startDragImmediately: Boolean, - onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit, + onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit, onDragDelta: (Float) -> Unit, onDragStopped: (velocity: Float) -> Unit, ): Modifier = composed { @@ -83,7 +84,7 @@ internal fun Modifier.multiPointerDraggable( val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown -> velocityTracker.resetTracking() - onDragStarted(startedPosition, pointersDown) + onDragStarted(size, startedPosition, pointersDown) } val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index eb5168bdd3cb..1a79522da05d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -25,8 +25,9 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.intermediateLayout import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.IntSize import androidx.compose.ui.zIndex @@ -44,14 +45,24 @@ internal class Scene( var content by mutableStateOf(content) var userActions by mutableStateOf(actions) var zIndex by mutableFloatStateOf(zIndex) - var size by mutableStateOf(IntSize.Zero) + var targetSize by mutableStateOf(IntSize.Zero) /** The shared values in this scene that are not tied to a specific element. */ val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>() @Composable + @OptIn(ExperimentalComposeUiApi::class) fun Content(modifier: Modifier = Modifier) { - Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) { + Box( + modifier + .zIndex(zIndex) + .intermediateLayout { measurable, constraints -> + targetSize = lookaheadSize + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { placeable.place(0, 0) } + } + .testTag(key.testTag) + ) { scope.content() } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt index 9a3a0aef30cb..9d71801be25b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round @@ -90,7 +91,7 @@ class SceneGestureHandler( internal var gestureWithPriority: Any? = null - internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) { + internal fun onDragStarted(pointersDown: Int, layoutSize: IntSize, startedPosition: Offset?) { if (isDrivingTransition) { // This [transition] was already driving the animation: simply take over it. // Stop animating and start from where the current offset. @@ -126,14 +127,14 @@ class SceneGestureHandler( // we will also have to make sure that we correctly handle overscroll. swipeTransition.absoluteDistance = when (orientation) { - Orientation.Horizontal -> layoutImpl.size.width - Orientation.Vertical -> layoutImpl.size.height + Orientation.Horizontal -> layoutSize.width + Orientation.Vertical -> layoutSize.height }.toFloat() val fromEdge = startedPosition?.let { position -> layoutImpl.edgeDetector.edge( - layoutImpl.size, + layoutSize, position.round(), layoutImpl.density, orientation, @@ -513,9 +514,9 @@ class SceneGestureHandler( private class SceneDraggableHandler( private val gestureHandler: SceneGestureHandler, ) : DraggableHandler { - override fun onDragStarted(startedPosition: Offset, pointersDown: Int) { + override fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) { gestureHandler.gestureWithPriority = this - gestureHandler.onDragStarted(pointersDown, startedPosition) + gestureHandler.onDragStarted(pointersDown, layoutSize, startedPosition) } override fun onDelta(pixels: Float) { @@ -647,7 +648,11 @@ class SceneNestedScrollHandler( canContinueScroll = { true }, onStart = { gestureHandler.gestureWithPriority = this - gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null) + gestureHandler.onDragStarted( + pointersDown = 1, + layoutSize = gestureHandler.currentScene.targetSize, + startedPosition = null, + ) }, onScroll = { offsetAvailable -> if (gestureHandler.gestureWithPriority != this) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 6edd1b6b923d..0b06953bc8e2 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -30,13 +30,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.layout.LookaheadScope -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.intermediateLayout import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastForEach +import com.android.compose.ui.util.lerp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -64,12 +66,6 @@ class SceneTransitionLayoutImpl( private val horizontalGestureHandler: SceneGestureHandler private val verticalGestureHandler: SceneGestureHandler - /** - * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have - * any scene configured or right before the first measure pass of the layout. - */ - @VisibleForTesting var size by mutableStateOf(IntSize.Zero) - init { setScenes(builder) @@ -157,15 +153,46 @@ class SceneTransitionLayoutImpl( } @Composable + @OptIn(ExperimentalComposeUiApi::class) internal fun Content(modifier: Modifier) { Box( modifier // Handle horizontal and vertical swipes on this layout. // Note: order here is important and will give a slight priority to the vertical // swipes. - .swipeToScene(gestureHandler(Orientation.Horizontal)) - .swipeToScene(gestureHandler(Orientation.Vertical)) - .onSizeChanged { size = it } + .swipeToScene(horizontalGestureHandler) + .swipeToScene(verticalGestureHandler) + // Animate the size of this layout. + .intermediateLayout { measurable, constraints -> + // Measure content normally. + val placeable = measurable.measure(constraints) + + val width: Int + val height: Int + val state = state.transitionState + if (state !is TransitionState.Transition || state.fromScene == state.toScene) { + width = placeable.width + height = placeable.height + } else { + // Interpolate the size. + val fromSize = scene(state.fromScene).targetSize + val toSize = scene(state.toScene).targetSize + + // Optimization: make sure we don't read state.progress if fromSize == + // toSize to avoid running this code every frame when the layout size does + // not change. + if (fromSize == toSize) { + width = fromSize.width + height = fromSize.height + } else { + val size = lerp(fromSize, toSize, state.progress) + width = size.width.coerceAtLeast(0) + height = size.height.coerceAtLeast(0) + } + } + + layout(width, height) { placeable.place(0, 0) } + } ) { LookaheadScope { val scenesToCompose = @@ -230,4 +257,9 @@ class SceneTransitionLayoutImpl( } internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene) + + @VisibleForTesting + fun setScenesTargetSizeForTest(size: IntSize) { + scenes.values.forEach { it.targetSize = size } + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt index 840800d838db..70534dde4f6f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt @@ -38,7 +38,7 @@ internal class EdgeTranslate( transition: TransitionState.Transition, value: Offset ): Offset { - val sceneSize = scene.size + val sceneSize = scene.targetSize val elementSize = sceneValues.targetSize if (elementSize == Element.SizeUnspecified) { return value diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt index 1e3d01108103..7ab2096b3d88 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt @@ -46,6 +46,7 @@ import org.junit.Test import org.junit.runner.RunWith private const val SCREEN_SIZE = 100f +private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) @RunWith(AndroidJUnit4::class) class SceneGestureHandlerTest { @@ -80,7 +81,7 @@ class SceneGestureHandlerTest { edgeDetector = DefaultEdgeDetector, coroutineScope = coroutineScope, ) - .also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) }, + .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) }, orientation = Orientation.Vertical, coroutineScope = coroutineScope, ) @@ -128,18 +129,21 @@ class SceneGestureHandlerTest { runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() } } + private fun DraggableHandler.onDragStarted() = + onDragStarted(layoutSize = LAYOUT_SIZE, startedPosition = Offset.Zero) + @Test fun testPreconditions() = runGestureTest { assertScene(currentScene = SceneA, isIdle = true) } @Test fun onDragStarted_shouldStartATransition() = runGestureTest { - draggable.onDragStarted(startedPosition = Offset.Zero) + draggable.onDragStarted() assertScene(currentScene = SceneA, isIdle = false) } @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - draggable.onDragStarted(startedPosition = Offset.Zero) + draggable.onDragStarted() assertScene(currentScene = SceneA, isIdle = false) val transition = transitionState as Transition @@ -152,7 +156,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - draggable.onDragStarted(startedPosition = Offset.Zero) + draggable.onDragStarted() assertScene(currentScene = SceneA, isIdle = false) draggable.onDelta(pixels = deltaInPixels10) @@ -170,7 +174,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - draggable.onDragStarted(startedPosition = Offset.Zero) + draggable.onDragStarted() assertScene(currentScene = SceneA, isIdle = false) draggable.onDelta(pixels = deltaInPixels10) @@ -188,7 +192,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest { - draggable.onDragStarted(startedPosition = Offset.Zero) + draggable.onDragStarted() assertScene(currentScene = SceneA, isIdle = false) draggable.onDragStopped(velocity = 0f) @@ -197,7 +201,7 @@ class SceneGestureHandlerTest { @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - draggable.onDragStarted(startedPosition = Offset.Zero) + draggable.onDragStarted() assertScene(currentScene = SceneA, isIdle = false) draggable.onDelta(pixels = deltaInPixels10) @@ -217,7 +221,7 @@ class SceneGestureHandlerTest { assertScene(currentScene = SceneC, isIdle = false) // Start a new gesture while the offset is animating - draggable.onDragStarted(startedPosition = Offset.Zero) + draggable.onDragStarted() assertThat(sceneGestureHandler.isAnimatingOffset).isFalse() } @@ -421,6 +425,7 @@ class SceneGestureHandlerTest { draggable.onDelta(deltaInPixels10) assertScene(currentScene = SceneA, isIdle = true) } + @Test fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest { draggable.onDragStopped(velocityThreshold) @@ -437,7 +442,7 @@ class SceneGestureHandlerTest { @Test fun startNestedScrollWhileDragging() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always) - draggable.onDragStarted(Offset.Zero) + draggable.onDragStarted() assertScene(currentScene = SceneA, isIdle = false) val transition = transitionState as Transition diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 5afd420a5e16..321cf637824a 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -18,6 +18,8 @@ package com.android.compose.animation.scene import androidx.activity.ComponentActivity import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -48,6 +50,7 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.test.assertSizeIsEqualTo import com.android.compose.test.subjects.DpOffsetSubject import com.android.compose.test.subjects.assertThat import com.google.common.truth.Truth.assertThat @@ -307,6 +310,26 @@ class SceneTransitionLayoutTest { assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) } + @Test + fun layoutSizeIsAnimated() { + val layoutTag = "layout" + rule.testTransition( + fromSceneContent = { Box(Modifier.size(200.dp, 100.dp)) }, + toSceneContent = { Box(Modifier.size(120.dp, 140.dp)) }, + transition = { + // 4 frames of animation. + spec = tween(4 * 16, easing = LinearEasing) + }, + layoutModifier = Modifier.testTag(layoutTag), + ) { + before { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(200.dp, 100.dp) } + at(16) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(180.dp, 110.dp) } + at(32) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(160.dp, 120.dp) } + at(48) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(140.dp, 130.dp) } + after { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(120.dp, 140.dp) } + } + } + private fun SemanticsNodeInteraction.offsetRelativeTo( other: SemanticsNodeInteraction, ): DpOffset { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt index 2a27763f1d5c..8cffcf6980cc 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt @@ -48,7 +48,7 @@ class EdgeTranslateTest { rule.testTransition( // The layout under test is 300dp x 300dp. layoutModifier = Modifier.size(300.dp), - fromSceneContent = {}, + fromSceneContent = { Box(Modifier.fillMaxSize()) }, toSceneContent = { // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp) Box(Modifier.fillMaxSize()) { diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index e0ae1be69aaf..06de2965f716 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -16,7 +16,6 @@ package com.android.compose.animation.scene -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -104,7 +103,7 @@ fun ComposeContentTestRule.testTransition( currentScene, onChangeScene, transitions { from(fromScene, to = toScene, transition) }, - layoutModifier.fillMaxSize(), + layoutModifier, ) { scene(fromScene, content = fromSceneContent) scene(toScene, content = toSceneContent) diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index d64a1312ac66..ced96f1aba98 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -441,9 +441,6 @@ object Flags { // TODO(b/270437894): Tracking Bug val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume") - // TODO(b/304506662): Tracking Bug - val MEDIA_DEVICE_NAME_FIX = releasedFlag("media_device_name_fix") - // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag("simulate_dock_through_charging") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 078fefff394a..c2aedca4ffbc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject @@ -44,6 +45,7 @@ constructor( sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, controller: NotificationStackScrollLayoutController, + notificationStackSizeCalculator: NotificationStackSizeCalculator, private val smartspaceViewModel: KeyguardSmartspaceViewModel, ) : NotificationStackScrollLayoutSection( @@ -53,6 +55,7 @@ constructor( sharedNotificationContainer, sharedNotificationContainerViewModel, controller, + notificationStackSizeCalculator, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index 00966f235a57..ea2bdf79a114 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel @@ -40,6 +41,7 @@ constructor( private val sharedNotificationContainer: SharedNotificationContainer, private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, private val controller: NotificationStackScrollLayoutController, + private val notificationStackSizeCalculator: NotificationStackSizeCalculator, ) : KeyguardSection() { private val placeHolderId = R.id.nssl_placeholder private var disposableHandle: DisposableHandle? = null @@ -69,6 +71,7 @@ constructor( sharedNotificationContainer, sharedNotificationContainerViewModel, controller, + notificationStackSizeCalculator, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index bf95c77229e9..dc2ad8d8f718 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject @@ -44,6 +45,7 @@ constructor( sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, controller: NotificationStackScrollLayoutController, + notificationStackSizeCalculator: NotificationStackSizeCalculator, private val smartspaceViewModel: KeyguardSmartspaceViewModel, ) : NotificationStackScrollLayoutSection( @@ -53,6 +55,7 @@ constructor( sharedNotificationContainer, sharedNotificationContainerViewModel, controller, + notificationStackSizeCalculator, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt index 2034d97f211c..dcbf670460ef 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt @@ -37,8 +37,6 @@ import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlagsClassic -import com.android.systemui.flags.Flags import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaDeviceData import com.android.systemui.media.controls.util.MediaControllerFactory @@ -70,7 +68,6 @@ constructor( @Main private val fgExecutor: Executor, @Background private val bgExecutor: Executor, dumpManager: DumpManager, - private val featureFlags: FeatureFlagsClassic, ) : MediaDataManager.Listener, Dumpable { private val listeners: MutableSet<Listener> = mutableSetOf() @@ -392,13 +389,6 @@ constructor( ) } - if (!featureFlags.isEnabled(Flags.MEDIA_DEVICE_NAME_FIX)) { - if (controller == null || routingSession != null) { - return routingSession?.name?.toString() ?: device?.name - } - return null - } - if (controller == null) { // In resume state, we don't have a controller - just use the device name return device?.name diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index 1ec43c5e3091..d277f32d5e54 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -60,7 +60,7 @@ constructor( } companion object { - @JvmField val GUTS_ANIMATION_DURATION = 500L + @JvmField val GUTS_ANIMATION_DURATION = 234L } /** A listener when the current dimensions of the player change */ @@ -234,7 +234,8 @@ constructor( currentStartLocation, currentEndLocation, currentTransitionProgress, - applyImmediately = false + applyImmediately = false, + isGutsAnimation = true, ) } @@ -254,7 +255,8 @@ constructor( currentStartLocation, currentEndLocation, currentTransitionProgress, - applyImmediately = immediate + applyImmediately = immediate, + isGutsAnimation = true, ) } @@ -414,7 +416,10 @@ constructor( * it's not available, it will recreate one by measuring, which may be expensive. */ @VisibleForTesting - fun obtainViewState(state: MediaHostState?): TransitionViewState? { + fun obtainViewState( + state: MediaHostState?, + isGutsAnimation: Boolean = false + ): TransitionViewState? { if (state == null || state.measurementInput == null) { return null } @@ -423,7 +428,7 @@ constructor( val viewState = viewStates[cacheKey] if (viewState != null) { // we already have cached this measurement, let's continue - if (state.squishFraction <= 1f) { + if (state.squishFraction <= 1f && !isGutsAnimation) { return squishViewState(viewState, state.squishFraction) } return viewState @@ -455,13 +460,14 @@ constructor( // Given that we have a measurement and a view, let's get (guaranteed) viewstates // from the start and end state and interpolate them - val startViewState = obtainViewState(startState) as TransitionViewState + val startViewState = obtainViewState(startState, isGutsAnimation) as TransitionViewState val endState = state.copy().also { it.expansion = 1.0f } - val endViewState = obtainViewState(endState) as TransitionViewState + val endViewState = obtainViewState(endState, isGutsAnimation) as TransitionViewState result = layoutController.getInterpolatedState(startViewState, endViewState, state.expansion) } - if (state.squishFraction <= 1f) { + // Skip the adjustments of squish view state if UMO changes due to guts animation. + if (state.squishFraction <= 1f && !isGutsAnimation) { return squishViewState(result, state.squishFraction) } return result @@ -521,7 +527,8 @@ constructor( @MediaLocation startLocation: Int, @MediaLocation endLocation: Int, transitionProgress: Float, - applyImmediately: Boolean + applyImmediately: Boolean, + isGutsAnimation: Boolean = false, ) = traceSection("MediaViewController#setCurrentState") { currentEndLocation = endLocation @@ -537,7 +544,7 @@ constructor( // Obtain the view state that we'd want to be at the end // The view might not be bound yet or has never been measured and in that case will be // reset once the state is fully available - var endViewState = obtainViewState(endHostState) ?: return + var endViewState = obtainViewState(endHostState, isGutsAnimation) ?: return endViewState = updateViewStateSize(endViewState, endLocation, tmpState2)!! layoutController.setMeasureState(endViewState) @@ -548,7 +555,7 @@ constructor( } val result: TransitionViewState - var startViewState = obtainViewState(startHostState) + var startViewState = obtainViewState(startHostState, isGutsAnimation) startViewState = updateViewStateSize(startViewState, startLocation, tmpState3) if (!endHostState.visible) { @@ -602,7 +609,8 @@ constructor( applyImmediately, shouldAnimate, animationDuration, - animationDelay + animationDelay, + isGutsAnimation, ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 285cb5a93b33..e9c930ae7614 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1483,16 +1483,16 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateMaxDisplayedNotifications(boolean recompute) { + if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + return; + } + if (recompute) { setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1)); } else { if (SPEW_LOGCAT) Log.d(TAG, "Skipping computeMaxKeyguardNotifications() by request"); } - if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) { - return; - } - if (isKeyguardShowing() && !mKeyguardBypassController.getBypassEnabled()) { mNotificationStackScrollLayoutController.setMaxDisplayedNotifications( mMaxAllowedKeyguardNotifications); diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt index e2e4556f59f7..8bab6696e2d3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -65,6 +65,10 @@ interface ShadeRepository { */ @Deprecated("Use ShadeInteractor instead") val legacyShadeTracking: StateFlow<Boolean> + /** Specifically tracks the user expanding the shade on the lockscreen only */ + @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead") + val legacyLockscreenShadeTracking: MutableStateFlow<Boolean> + /** * QuickSettingsController.mTracking as a flow. "Tracking" means that the user is moving quick * settings up or down with a pointer. Going forward, this concept will be replaced by checks @@ -106,6 +110,9 @@ interface ShadeRepository { /** Sets whether the user is moving the shade with a pointer */ fun setLegacyShadeTracking(tracking: Boolean) + /** Sets whether the user is moving the shade with a pointer, on lockscreen only */ + fun setLegacyLockscreenShadeTracking(tracking: Boolean) + /** Amount shade has expanded with regard to the UDFPS location */ val udfpsTransitionToFullShadeProgress: StateFlow<Float> @@ -177,6 +184,8 @@ constructor(shadeExpansionStateManager: ShadeExpansionStateManager) : ShadeRepos @Deprecated("Use ShadeInteractor instead") override val legacyShadeTracking: StateFlow<Boolean> = _legacyShadeTracking.asStateFlow() + override val legacyLockscreenShadeTracking = MutableStateFlow(false) + private val _legacyQsTracking = MutableStateFlow(false) @Deprecated("Use ShadeInteractor instead") override val legacyQsTracking: StateFlow<Boolean> = _legacyQsTracking.asStateFlow() @@ -212,6 +221,11 @@ constructor(shadeExpansionStateManager: ShadeExpansionStateManager) : ShadeRepos _legacyShadeTracking.value = tracking } + @Deprecated("Should only be called by NPVC and tests") + override fun setLegacyLockscreenShadeTracking(tracking: Boolean) { + legacyLockscreenShadeTracking.value = tracking + } + override fun setQsExpansion(qsExpansion: Float) { _qsExpansion.value = qsExpansion } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index b2ffeb3f2925..d687ef64aec4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -72,7 +72,7 @@ constructor( userSetupRepository: UserSetupRepository, userSwitcherInteractor: UserSwitcherInteractor, sharedNotificationContainerInteractor: SharedNotificationContainerInteractor, - repository: ShadeRepository, + private val repository: ShadeRepository, ) { /** Emits true if the shade is currently allowed and false otherwise. */ val isShadeEnabled: StateFlow<Boolean> = @@ -185,7 +185,15 @@ constructor( if (sceneContainerFlags.isEnabled()) { sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.Shade) } else { - userInteractingFlow(repository.legacyShadeTracking, repository.legacyShadeExpansion) + combine( + userInteractingFlow( + repository.legacyShadeTracking, + repository.legacyShadeExpansion + ), + repository.legacyLockscreenShadeTracking + ) { legacyShadeTracking, legacyLockscreenShadeTracking -> + legacyShadeTracking || legacyLockscreenShadeTracking + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index bf722af80934..2e3f3f894687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -340,6 +340,7 @@ constructor( ) nsslController.resetScrollPosition() nsslController.resetCheckSnoozeLeavebehind() + shadeRepository.setLegacyLockscreenShadeTracking(false) setDragDownAmountAnimated(0f) } @@ -366,6 +367,7 @@ constructor( cancel() } } + shadeRepository.setLegacyLockscreenShadeTracking(true) } /** Do we need a falsing check currently? */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 3f080c247924..0e83c78edb1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -680,7 +680,7 @@ public class NotificationLockscreenUserManagerImpl implements } NotificationEntry entry = mCommonNotifCollectionLazy.get().getEntry(key); if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) { - return entry != null + return entry != null && entry.getRanking().getChannel() != null && entry.getRanking().getChannel().getLockscreenVisibility() == Notification.VISIBILITY_PRIVATE; } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index a0ffba304027..14ec08f3545f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -253,6 +253,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private NotificationLogger.OnChildLocationsChangedListener mListener; private OnOverscrollTopChangedListener mOverscrollTopChangedListener; private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; + private Runnable mOnHeightChangedRunnable; private OnEmptySpaceClickListener mOnEmptySpaceClickListener; private boolean mNeedsAnimation; private boolean mTopPaddingNeedsAnimation; @@ -1121,6 +1122,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (mOnHeightChangedListener != null) { mOnHeightChangedListener.onHeightChanged(view, needsAnimation); } + + if (mOnHeightChangedRunnable != null) { + mOnHeightChangedRunnable.run(); + } } public boolean isPulseExpanding() { @@ -4252,6 +4257,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable this.mOnHeightChangedListener = onHeightChangedListener; } + void setOnHeightChangedRunnable(Runnable r) { + this.mOnHeightChangedRunnable = r; + } + void onChildAnimationFinished() { setAnimationRunning(false); requestChildrenUpdate(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 99b3a005ab0c..2cf0c262c528 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -20,6 +20,7 @@ import static android.service.notification.NotificationStats.DISMISSAL_SHADE; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; import static com.android.app.animation.Interpolators.STANDARD; + import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; @@ -34,7 +35,6 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.ObjectAnimator; import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Point; import android.os.Trace; import android.os.UserHandle; @@ -65,9 +65,7 @@ import com.android.systemui.Gefingerpoken; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; -import com.android.systemui.common.ui.ConfigurationState; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; @@ -94,7 +92,6 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; -import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -112,7 +109,6 @@ import com.android.systemui.statusbar.notification.collection.render.Notificatio import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.dagger.SilentHeader; import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; @@ -122,11 +118,9 @@ import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.KeyguardBypassController; -import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -960,6 +954,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setOnHeightChangedListener(listener); } + /** + * Invoked in addition to {@see #setOnHeightChangedListener} + */ + public void setOnHeightChangedRunnable(Runnable r) { + mView.setOnHeightChangedRunnable(r); + } + public void setOverscrollTopChangedListener( OnOverscrollTopChangedListener listener) { mView.setOverscrollTopChangedListener(listener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt index 57cea5d3b31e..eb1c17aaca78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt @@ -18,13 +18,15 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import android.content.Context -import com.android.systemui.res.R import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.SplitShadeStateController import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -43,6 +45,10 @@ constructor( private val _topPosition = MutableStateFlow(0f) val topPosition = _topPosition.asStateFlow() + private val _notificationStackChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + /** An internal modification was made to notifications */ + val notificationStackChanged = _notificationStackChanged.asSharedFlow() + val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = configurationRepository.onAnyConfigurationChange .onStart { emit(Unit) } @@ -72,6 +78,11 @@ constructor( _topPosition.value = top } + /** An internal modification was made to notifications */ + fun notificationStackChanged() { + _notificationStackChanged.tryEmit(Unit) + } + data class ConfigurationBasedDimensions( val useSplitShade: Boolean, val useLargeScreenHeader: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index a1a0ccac3500..0ff1bec84d16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import kotlinx.coroutines.DisposableHandle @@ -33,6 +34,7 @@ object SharedNotificationContainerBinder { view: SharedNotificationContainer, viewModel: SharedNotificationContainerViewModel, controller: NotificationStackScrollLayoutController, + notificationStackSizeCalculator: NotificationStackSizeCalculator, ): DisposableHandle { val disposableHandle = view.repeatWhenAttached { @@ -54,9 +56,16 @@ object SharedNotificationContainerBinder { } launch { - viewModel.maxNotifications.collect { - controller.setMaxDisplayedNotifications(it) - } + viewModel + .getMaxNotifications { space -> + notificationStackSizeCalculator.computeMaxKeyguardNotifications( + controller.getView(), + space, + 0f, // Vertical space for shelf is already accounted for + controller.getShelfHeight().toFloat(), + ) + } + .collect { controller.setMaxDisplayedNotifications(it) } } launch { @@ -70,9 +79,12 @@ object SharedNotificationContainerBinder { } } + controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + return object : DisposableHandle { override fun dispose() { disposableHandle.dispose() + controller.setOnHeightChangedRunnable(null) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index b86b5dcc7939..d6b6f75b3186 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -22,28 +22,26 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart /** View-model for the shared notification container, used by both the shade and keyguard spaces */ class SharedNotificationContainerViewModel @Inject constructor( - interactor: SharedNotificationContainerInteractor, + private val interactor: SharedNotificationContainerInteractor, keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - controller: NotificationStackScrollLayoutController, - shadeInteractor: ShadeInteractor, + private val shadeInteractor: ShadeInteractor, ) { private val statesForConstrainedNotifications = setOf( @@ -151,24 +149,46 @@ constructor( * When on keyguard, there is limited space to display notifications so calculate how many could * be shown. Otherwise, there is no limit since the vertical space will be scrollable. * - * TODO: b/296606746 - Need to rerun logic when notifs change + * When expanding or when the user is interacting with the shade, keep the count stable; do not + * emit a value. */ - val maxNotifications: Flow<Int> = - combine(isOnLockscreen, shadeInteractor.shadeExpansion, position) { - onLockscreen, - shadeExpansion, - positionInfo -> - if (onLockscreen && shadeExpansion < 1f) { - notificationStackSizeCalculator.computeMaxKeyguardNotifications( - controller.getView(), - positionInfo.bottom - positionInfo.top, - 0f, // Vertical space for shelf is already accounted for - controller.getShelfHeight().toFloat(), - ) - } else { - -1 // No limit + fun getMaxNotifications(calculateSpace: (Float) -> Int): Flow<Int> { + // When to limit notifications: on lockscreen with an unexpanded shade. Also, recalculate + // when the notification stack has changed internally + val limitedNotifications = + combineTransform( + isOnLockscreen, + position, + shadeInteractor.shadeExpansion, + interactor.notificationStackChanged.onStart { emit(Unit) }, + ) { isOnLockscreen, position, shadeExpansion, _ -> + if (isOnLockscreen && shadeExpansion == 0f) { + emit(calculateSpace(position.bottom - position.top)) + } } - } + + // When to show unlimited notifications: When the shade is fully expanded and the user is + // not actively dragging the shade + val unlimitedNotifications = + combineTransform( + shadeInteractor.shadeExpansion, + shadeInteractor.isUserInteracting, + ) { shadeExpansion, isUserInteracting -> + if (shadeExpansion == 1f && !isUserInteracting) { + emit(-1) + } + } + + return merge( + limitedNotifications, + unlimitedNotifications, + ) + .distinctUntilChanged() + } + + fun notificationStackChanged() { + interactor.notificationStackChanged() + } data class ConfigurationBasedDimensions( val marginStart: Int, diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt index db4ab7edbcf1..5b9161593703 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt @@ -25,6 +25,16 @@ import com.android.app.animation.Interpolators * The fraction after which we start fading in when going from a gone widget to a visible one */ private const val GONE_FADE_FRACTION = 0.8f +/** + * The fraction after which we start fading in going from a gone widget to a visible one in guts + * animation. + */ +private const val GONE_FADE_GUTS_FRACTION = 0.286f +/** + * The fraction before which we fade out when going from a visible widget to a gone one in guts + * animation. + */ +private const val VISIBLE_FADE_GUTS_FRACTION = 0.355f /** * The amont we're scaling appearing views @@ -45,6 +55,7 @@ open class TransitionLayoutController { private var animationStartState: TransitionViewState? = null private var state = TransitionViewState() private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) + private var isGutsAnimation = false private var currentHeight: Int = 0 private var currentWidth: Int = 0 var sizeChangedListener: ((Int, Int) -> Unit)? = null @@ -152,15 +163,6 @@ open class TransitionLayoutController { // this looks quite ugly val nowGone: Boolean if (widgetStart.gone) { - - // Only fade it in at the very end - alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress) - nowGone = progress < GONE_FADE_FRACTION - - // Scale it just a little, not all the way - val endScale = widgetEnd.scale - newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress) - // don't clip widthProgress = 1.0f @@ -168,25 +170,52 @@ open class TransitionLayoutController { resultMeasureWidth = widgetEnd.measureWidth resultMeasureHeight = widgetEnd.measureHeight - // Let's make sure we're centering the view in the gone view instead of having - // the left at 0 - resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f, - widgetEnd.x, - progress) - resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f, - widgetEnd.y, - progress) + if (isGutsAnimation) { + // if animation is open/close guts, fade in starts early. + alphaProgress = MathUtils.map( + GONE_FADE_GUTS_FRACTION, + 1.0f, + 0.0f, + 1.0f, + progress + ) + nowGone = progress < GONE_FADE_GUTS_FRACTION + + // Do not change scale of widget. + newScale = 1.0f + + // We do not want any horizontal or vertical movement. + resultX = widgetStart.x + resultY = widgetStart.y + } else { + // Only fade it in at the very end + alphaProgress = MathUtils.map( + GONE_FADE_FRACTION, + 1.0f, + 0.0f, + 1.0f, + progress + ) + nowGone = progress < GONE_FADE_FRACTION + + // Scale it just a little, not all the way + val endScale = widgetEnd.scale + newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress) + + // Let's make sure we're centering the view in the gone view instead of + // having the left at 0 + resultX = MathUtils.lerp( + widgetStart.x - resultMeasureWidth / 2.0f, + widgetEnd.x, + progress + ) + resultY = MathUtils.lerp( + widgetStart.y - resultMeasureHeight / 2.0f, + widgetEnd.y, + progress + ) + } } else { - - // Fadeout in the very beginning - alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f, - progress) - nowGone = progress > 1.0f - GONE_FADE_FRACTION - - // Scale it just a little, not all the way - val startScale = widgetStart.scale - newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress) - // Don't clip widthProgress = 0.0f @@ -194,14 +223,54 @@ open class TransitionLayoutController { resultMeasureWidth = widgetStart.measureWidth resultMeasureHeight = widgetStart.measureHeight - // Let's make sure we're centering the view in the gone view instead of having - // the left at 0 - resultX = MathUtils.lerp(widgetStart.x, - widgetEnd.x - resultMeasureWidth / 2.0f, - progress) - resultY = MathUtils.lerp(widgetStart.y, - widgetEnd.y - resultMeasureHeight / 2.0f, - progress) + // Fadeout in the very beginning + if (isGutsAnimation) { + alphaProgress = MathUtils.map( + 0.0f, + VISIBLE_FADE_GUTS_FRACTION, + 0.0f, + 1.0f, + progress + ) + nowGone = progress > VISIBLE_FADE_GUTS_FRACTION + + // Do not change scale of widget during open/close guts animation. + newScale = 1.0f + + // We do not want any horizontal or vertical movement. + resultX = widgetEnd.x + resultY = widgetEnd.y + } else { + alphaProgress = MathUtils.map( + 0.0f, + 1.0f - GONE_FADE_FRACTION, + 0.0f, + 1.0f, + progress + ) + nowGone = progress > 1.0f - GONE_FADE_FRACTION + + // Scale it just a little, not all the way + val startScale = widgetStart.scale + newScale = MathUtils.lerp( + startScale, + startScale * GONE_SCALE_AMOUNT, + progress + ) + + // Let's make sure we're centering the view in the gone view instead of + // having the left at 0 + resultX = MathUtils.lerp( + widgetStart.x, + widgetEnd.x - resultMeasureWidth / 2.0f, + progress + ) + resultY = MathUtils.lerp( + widgetStart.y, + widgetEnd.y - resultMeasureHeight / 2.0f, + progress + ) + } } resultWidgetState.gone = nowGone } else { @@ -279,8 +348,10 @@ open class TransitionLayoutController { applyImmediately: Boolean, animate: Boolean, duration: Long = 0, - delay: Long = 0 + delay: Long = 0, + isGuts: Boolean, ) { + isGutsAnimation = isGuts val animated = animate && currentState.width != 0 this.state = state.copy() if (applyImmediately || transitionLayout == null) { @@ -291,6 +362,8 @@ open class TransitionLayoutController { animationStartState = currentState.copy() animator.duration = duration animator.startDelay = delay + animator.interpolator = + if (isGutsAnimation) Interpolators.LINEAR else Interpolators.FAST_OUT_SLOW_IN animator.start() } else if (!animator.isRunning) { applyStateToLayout(this.state) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt index b101acf3418b..437a35f2fab5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt @@ -38,8 +38,6 @@ import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FakeFeatureFlagsClassic -import com.android.systemui.flags.Flags import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaDeviceData @@ -112,7 +110,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { private lateinit var session: MediaSession private lateinit var mediaData: MediaData @JvmField @Rule val mockito = MockitoJUnit.rule() - private val featureFlags = FakeFeatureFlagsClassic() @Before fun setUp() { @@ -131,7 +128,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fakeFgExecutor, fakeBgExecutor, dumpster, - featureFlags, ) manager.addListener(listener) @@ -150,7 +146,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken) whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller) setupLeAudioConfiguration(false) - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, false) } @After @@ -463,7 +458,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun mr2ReturnsSystemRouteWithNullName_isPhone_usePhoneName() { - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true) // When the routing session name is null, and is a system session for a PhoneMediaDevice val phoneDevice = mock(PhoneMediaDevice::class.java) whenever(phoneDevice.iconWithoutBackground).thenReturn(icon) @@ -489,7 +483,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun mr2ReturnsSystemRouteWithNullName_useSelectedRouteName() { - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true) // When the routing session does not have a name, and is a system session whenever(route.name).thenReturn(null) whenever(mr2.getSelectedRoutes(any())).thenReturn(listOf(selectedRoute)) @@ -725,101 +718,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { assertThat(data.showBroadcastButton).isFalse() } - // Duplicates of above tests with MEDIA_DEVICE_NAME_FIX enabled - - @Test - fun loadMediaDataWithNullToken_withNameFix() { - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true) - manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null)) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - val data = captureDeviceData(KEY) - assertThat(data.enabled).isTrue() - assertThat(data.name).isEqualTo(DEVICE_NAME) - } - - @Test - fun onAboutToConnectDeviceAdded_findsDeviceInfoFromAddress_withNameFix() { - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true) - manager.onMediaDataLoaded(KEY, null, mediaData) - // Run and reset the executors and listeners so we only focus on new events. - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - reset(listener) - - // Ensure we'll get device info when using the address - val fullMediaDevice = mock(MediaDevice::class.java) - val address = "fakeAddress" - val nameFromDevice = "nameFromDevice" - val iconFromDevice = mock(Drawable::class.java) - whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(fullMediaDevice) - whenever(fullMediaDevice.name).thenReturn(nameFromDevice) - whenever(fullMediaDevice.iconWithoutBackground).thenReturn(iconFromDevice) - - // WHEN the about-to-connect device changes to non-null - val deviceCallback = captureCallback() - val nameFromParam = "nameFromParam" - val iconFromParam = mock(Drawable::class.java) - deviceCallback.onAboutToConnectDeviceAdded(address, nameFromParam, iconFromParam) - assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1) - - // THEN the about-to-connect device based on the address is returned - val data = captureDeviceData(KEY) - assertThat(data.enabled).isTrue() - assertThat(data.name).isEqualTo(nameFromDevice) - assertThat(data.name).isNotEqualTo(nameFromParam) - assertThat(data.icon).isEqualTo(iconFromDevice) - assertThat(data.icon).isNotEqualTo(iconFromParam) - } - - @Test - fun deviceNameFromMR2RouteInfo_withNameFix() { - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true) - // GIVEN that MR2Manager returns a valid routing session - whenever(route.name).thenReturn(REMOTE_DEVICE_NAME) - // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - // THEN it uses the route name (instead of device name) - val data = captureDeviceData(KEY) - assertThat(data.enabled).isTrue() - assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME) - } - - @Test - fun deviceDisabledWhenMR2ReturnsNullRouteInfo_withNameFix() { - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true) - // GIVEN that MR2Manager returns null for routing session - whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) - // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - // THEN the device is disabled and name is set to null - val data = captureDeviceData(KEY) - assertThat(data.enabled).isFalse() - assertThat(data.name).isNull() - } - - @Test - fun mr2ReturnsNonSystemRouteWithNullName_useLocalDeviceName_withNameFix() { - featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true) - // GIVEN that MR2Manager returns a routing session that does not have a name - whenever(route.name).thenReturn(null) - whenever(route.isSystemSession).thenReturn(false) - // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - // THEN the device is enabled and uses the current connected device name - val data = captureDeviceData(KEY) - assertThat(data.name).isEqualTo(DEVICE_NAME) - assertThat(data.enabled).isTrue() - } - - // End duplicate tests - private fun captureCallback(): LocalMediaManager.DeviceCallback { val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java) verify(lmm).registerCallback(captor.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt index e920687753fd..20b19fd16f4f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt @@ -158,6 +158,15 @@ class ShadeRepositoryImplTest : SysuiTestCase() { } @Test + fun updateLegacyLockscreenShadeTracking() = + testScope.runTest { + assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(false) + + underTest.setLegacyLockscreenShadeTracking(true) + assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(true) + } + + @Test fun updateLegacyQsTracking() = testScope.runTest { assertThat(underTest.legacyQsTracking.value).isEqualTo(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index a1425f811a84..ae3214267ff5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -323,6 +323,20 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { } @Test + public void testCurrentUserPrivateNotificationsNullChannel() { + // GIVEN current user allows private notifications to show + mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, + mCurrentUser.id); + changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); + + mCurrentUserNotif.setRanking(new RankingBuilder(mCurrentUserNotif.getRanking()) + .setChannel(null) + .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build()); + // THEN the notification is not redacted + assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + } + + @Test public void testWorkPrivateNotificationsRedacted() { // GIVEN work profile doesn't private notifications to show mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 6203531cabab..e91d6d724a73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -83,6 +83,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -906,6 +907,20 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { assertEquals(bottomImeInset, mStackScrollerInternal.mBottomInset); } + @Test + public void testSetMaxDisplayedNotifications_notifiesListeners() { + ExpandableView.OnHeightChangedListener listener = + mock(ExpandableView.OnHeightChangedListener.class); + Runnable runnable = mock(Runnable.class); + mStackScroller.setOnHeightChangedListener(listener); + mStackScroller.setOnHeightChangedRunnable(runnable); + + mStackScroller.setMaxDisplayedNotifications(50); + + verify(listener).onHeightChanged(mNotificationShelf, false); + verify(runnable).run(); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 22553dfc4cb1..db8f21714964 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -40,13 +40,8 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.res.R import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.user.domain.UserDomainLayerModule -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -84,25 +79,13 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } } - private val notificationStackSizeCalculator: NotificationStackSizeCalculator = mock() - private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController = - mock { - whenever(view).thenReturn(mock()) - whenever(shelfHeight).thenReturn(0) - } - private val testComponent: TestComponent = DaggerSharedNotificationContainerViewModelTest_TestComponent.factory() .create( test = this, featureFlags = FakeFeatureFlagsClassicModule { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }, - mocks = - TestMocksModule( - notificationStackSizeCalculator = notificationStackSizeCalculator, - notificationStackScrollLayoutController = - notificationStackScrollLayoutController, - ) + mocks = TestMocksModule(), ) @Test @@ -336,17 +319,31 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { @Test fun maxNotificationsOnLockscreen() = testComponent.runTest { - whenever( - notificationStackSizeCalculator.computeMaxKeyguardNotifications( - any(), - any(), - any(), - any() - ) - ) - .thenReturn(10) + var notificationCount = 10 + val maxNotifications by + collectLastValue(underTest.getMaxNotifications { notificationCount }) + + showLockscreen() + + overrideResource(R.bool.config_use_split_notification_shade, false) + configurationRepository.onAnyConfigurationChange() + keyguardInteractor.sharedNotificationContainerPosition.value = + SharedNotificationContainerPosition(top = 1f, bottom = 2f) + + assertThat(maxNotifications).isEqualTo(10) + + // Also updates when directly requested (as it would from NotificationStackScrollLayout) + notificationCount = 25 + sharedNotificationContainerInteractor.notificationStackChanged() + assertThat(maxNotifications).isEqualTo(25) + } - val maxNotifications by collectLastValue(underTest.maxNotifications) + @Test + fun maxNotificationsOnLockscreen_DoesNotUpdateWhenUserInteracting() = + testComponent.runTest { + var notificationCount = 10 + val maxNotifications by + collectLastValue(underTest.getMaxNotifications { notificationCount }) showLockscreen() @@ -356,21 +353,30 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { SharedNotificationContainerPosition(top = 1f, bottom = 2f) assertThat(maxNotifications).isEqualTo(10) + + // Shade expanding... still 10 + shadeRepository.setLockscreenShadeExpansion(0.5f) + assertThat(maxNotifications).isEqualTo(10) + + notificationCount = 25 + + // When shade is expanding by user interaction + shadeRepository.setLegacyLockscreenShadeTracking(true) + + // Should still be 10, since the user is interacting + assertThat(maxNotifications).isEqualTo(10) + + shadeRepository.setLegacyLockscreenShadeTracking(false) + shadeRepository.setLockscreenShadeExpansion(0f) + + // Stopped tracking, show 25 + assertThat(maxNotifications).isEqualTo(25) } @Test fun maxNotificationsOnShade() = testComponent.runTest { - whenever( - notificationStackSizeCalculator.computeMaxKeyguardNotifications( - any(), - any(), - any(), - any() - ) - ) - .thenReturn(10) - val maxNotifications by collectLastValue(underTest.maxNotifications) + val maxNotifications by collectLastValue(underTest.getMaxNotifications { 10 }) // Show lockscreen with shade expanded showLockscreenWithShadeExpanded() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt index 800593fe61a1..02318abe8488 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt @@ -59,6 +59,8 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { private val _legacyIsQsExpanded = MutableStateFlow(false) @Deprecated("Use ShadeInteractor instead") override val legacyIsQsExpanded = _legacyIsQsExpanded + override val legacyLockscreenShadeTracking = MutableStateFlow(false) + @Deprecated("Use ShadeInteractor instead") override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) { _legacyIsQsExpanded.value = legacyIsQsExpanded @@ -81,6 +83,11 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { _legacyShadeTracking.value = tracking } + @Deprecated("Should only be called by NPVC and tests") + override fun setLegacyLockscreenShadeTracking(tracking: Boolean) { + legacyLockscreenShadeTracking.value = tracking + } + fun setShadeModel(model: ShadeModel) { _shadeModel.value = model } diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS index 83143a431406..5295ec82e3c3 100644 --- a/services/companion/java/com/android/server/companion/virtual/OWNERS +++ b/services/companion/java/com/android/server/companion/virtual/OWNERS @@ -1,3 +1,5 @@ +# Bug component: 1171888 + set noparent ogunwale@google.com diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 15fc2dc15d02..f6835feeea16 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -3651,7 +3651,26 @@ class StorageManagerService extends IStorageManager.Stub Watchdog.getInstance().pauseWatchingMonitorsFor( SLOW_OPERATION_WATCHDOG_TIMEOUT_MS, "#close might be slow"); if (mMounted) { - mVold.unmountAppFuse(uid, mountId); + BackgroundThread.getHandler().post(() -> { + try { + // We need to run the unmount on a separate thread to + // prevent a possible deadlock, where: + // 1. AppFuseThread (this thread) tries to call into vold + // 2. the vold lock is held by another thread, which called: + // mVold.openAppFuseFile() + // as part of that call, vold calls open() on the + // underlying file, which is a call that needs to be + // handled by the AppFuseThread, which is stuck waiting + // for the vold lock (see 1.) + // It is safe to do the unmount asynchronously, because the mount + // path we use is never reused during the current boot cycle; + // see mNextAppFuseName. Also,we have anyway stopped serving + // requests at this point. + mVold.unmountAppFuse(uid, mountId); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + }); mMounted = false; } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b99a98fe6e8b..f92af6780883 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -599,6 +599,9 @@ public class ActivityManagerService extends IActivityManager.Stub private static final String INTENT_REMOTE_BUGREPORT_FINISHED = "com.android.internal.intent.action.REMOTE_BUGREPORT_FINISHED"; + public static final String DATA_FILE_PATH_HEADER = "Data File: "; + public static final String DATA_FILE_PATH_FOOTER = "End Data File\n"; + // If set, we will push process association information in to procstats. static final boolean TRACK_PROCSTATS_ASSOCIATIONS = true; @@ -9595,17 +9598,33 @@ public class ActivityManagerService extends IActivityManager.Stub : Settings.Global.getInt(mContext.getContentResolver(), logcatSetting, 0); int dropboxMaxSize = Settings.Global.getInt( mContext.getContentResolver(), maxBytesSetting, DROPBOX_DEFAULT_MAX_SIZE); - int maxDataFileSize = dropboxMaxSize - sb.length() - - lines * RESERVED_BYTES_PER_LOGCAT_LINE; - if (dataFile != null && maxDataFileSize > 0) { - try { - sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize, - "\n\n[[TRUNCATED]]")); - } catch (IOException e) { - Slog.e(TAG, "Error reading " + dataFile, e); + if (dataFile != null) { + // Attach the stack traces file to the report so collectors can load them + // by file if they have access. + sb.append(DATA_FILE_PATH_HEADER) + .append(dataFile.getAbsolutePath()).append('\n'); + + int maxDataFileSize = dropboxMaxSize + - sb.length() + - lines * RESERVED_BYTES_PER_LOGCAT_LINE + - DATA_FILE_PATH_FOOTER.length(); + + if (maxDataFileSize > 0) { + // Inline dataFile contents if there is room. + try { + sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize, + "\n\n[[TRUNCATED]]\n")); + } catch (IOException e) { + Slog.e(TAG, "Error reading " + dataFile, e); + } } + + // Always append the footer, even there wasn't enough space to inline the + // dataFile contents. + sb.append(DATA_FILE_PATH_FOOTER); } + if (crashInfo != null && crashInfo.stackTrace != null) { sb.append(crashInfo.stackTrace); } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index e41b6ae28e80..3ce92bc2737e 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -265,13 +265,6 @@ class BroadcastProcessQueue { @Nullable public BroadcastRecord enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, @NonNull BroadcastConsumer deferredStatesApplyConsumer) { - // When updateDeferredStates() has already applied a deferred state to - // all pending items, apply to this new broadcast too - if (mLastDeferredStates && record.deferUntilActive - && (record.getDeliveryState(recordIndex) == BroadcastRecord.DELIVERY_PENDING)) { - deferredStatesApplyConsumer.accept(record, recordIndex); - } - // Ignore FLAG_RECEIVER_REPLACE_PENDING if the sender specified the policy using the // BroadcastOptions delivery group APIs. if (record.isReplacePending() @@ -294,6 +287,13 @@ class BroadcastProcessQueue { // with implicit responsiveness expectations. getQueueForBroadcast(record).addLast(newBroadcastArgs); onBroadcastEnqueued(record, recordIndex); + + // When updateDeferredStates() has already applied a deferred state to + // all pending items, apply to this new broadcast too + if (mLastDeferredStates && shouldBeDeferred() + && (record.getDeliveryState(recordIndex) == BroadcastRecord.DELIVERY_PENDING)) { + deferredStatesApplyConsumer.accept(record, recordIndex); + } return null; } @@ -1235,32 +1235,45 @@ class BroadcastProcessQueue { } /** - * Update {@link BroadcastRecord.DELIVERY_DEFERRED} states of all our + * Update {@link BroadcastRecord#DELIVERY_DEFERRED} states of all our * pending broadcasts, when needed. */ void updateDeferredStates(@NonNull BroadcastConsumer applyConsumer, @NonNull BroadcastConsumer clearConsumer) { // When all we have pending is deferred broadcasts, and we're cached, // then we want everything to be marked deferred - final boolean wantDeferredStates = (mCountDeferred > 0) - && (mCountDeferred == mCountEnqueued) && mProcessFreezable; + final boolean wantDeferredStates = shouldBeDeferred(); if (mLastDeferredStates != wantDeferredStates) { mLastDeferredStates = wantDeferredStates; if (wantDeferredStates) { forEachMatchingBroadcast((r, i) -> { - return r.deferUntilActive - && (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_PENDING); + return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_PENDING); }, applyConsumer, false); } else { forEachMatchingBroadcast((r, i) -> { - return r.deferUntilActive - && (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED); + return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED); }, clearConsumer, false); } } } + void clearDeferredStates(@NonNull BroadcastConsumer clearConsumer) { + if (mLastDeferredStates) { + mLastDeferredStates = false; + forEachMatchingBroadcast((r, i) -> { + return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED); + }, clearConsumer, false); + } + } + + @VisibleForTesting + boolean shouldBeDeferred() { + if (mRunnableAtInvalidated) updateRunnableAt(); + return mRunnableAtReason == REASON_CACHED + || mRunnableAtReason == REASON_CACHED_INFINITE_DEFER; + } + /** * Check overall health, confirming things are in a reasonable state and * that we're not wedged. diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index b48169788180..5b54561c2164 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -479,6 +479,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { break; } + // Clear the deferred state of broadcasts in this queue as we are just about to + // deliver broadcasts to this process. + queue.clearDeferredStates(mBroadcastConsumerDeferClear); + // We might not have heard about a newly running process yet, so // consider refreshing if we think we're cold updateWarmProcess(queue); @@ -1567,12 +1571,14 @@ class BroadcastQueueModernImpl extends BroadcastQueue { r.resultExtras = null; }; - private final BroadcastConsumer mBroadcastConsumerDeferApply = (r, i) -> { + @VisibleForTesting + final BroadcastConsumer mBroadcastConsumerDeferApply = (r, i) -> { setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_DEFERRED, "mBroadcastConsumerDeferApply"); }; - private final BroadcastConsumer mBroadcastConsumerDeferClear = (r, i) -> { + @VisibleForTesting + final BroadcastConsumer mBroadcastConsumerDeferClear = (r, i) -> { setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_PENDING, "mBroadcastConsumerDeferClear"); }; diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index a0a7b2b48725..d0ab287785e3 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1472,10 +1472,13 @@ public final class CachedAppOptimizer { } return; } + boolean processFreezableChangeReported = false; if (opt.isPendingFreeze()) { // Remove pending DO_FREEZE message mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app); opt.setPendingFreeze(false); + reportProcessFreezableChangedLocked(app); + processFreezableChangeReported = true; if (DEBUG_FREEZER) { Slog.d(TAG_AM, "Cancel freezing " + pid + " " + app.processName); } @@ -1524,7 +1527,9 @@ public final class CachedAppOptimizer { if (processKilled) { return; } - reportProcessFreezableChangedLocked(app); + if (!processFreezableChangeReported) { + reportProcessFreezableChangedLocked(app); + } long freezeTime = opt.getFreezeUnfreezeTime(); diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java index 51cb9505ed4f..5c8dd0d427f9 100644 --- a/services/core/java/com/android/server/audio/AdiDeviceState.java +++ b/services/core/java/com/android/server/audio/AdiDeviceState.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; +import android.media.Utils; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -167,8 +168,9 @@ import java.util.Objects; public String toString() { return "type: " + mDeviceType + " internal type: 0x" + Integer.toHexString(mInternalDeviceType) - + " addr: " + mDeviceAddress + " bt audio type: " - + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory) + + " addr: " + Utils.anonymizeBluetoothAddress(mInternalDeviceType, mDeviceAddress) + + " bt audio type: " + + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory) + " enabled: " + mSAEnabled + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled; } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 7ba0827f2016..e9b102bc67b8 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -48,6 +48,7 @@ import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.MediaMetrics; import android.media.MediaRecorder.AudioSource; +import android.media.Utils; import android.media.audiopolicy.AudioProductStrategy; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; @@ -477,7 +478,7 @@ public class AudioDeviceInventory { return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType) + " (" + AudioSystem.getDeviceName(mDeviceType) + ") name:" + mDeviceName - + " addr:" + mDeviceAddress + + " addr:" + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceAddress) + " codec: " + Integer.toHexString(mDeviceCodecFormat) + " peer addr:" + mPeerDeviceAddress + " group:" + mGroupId @@ -532,7 +533,7 @@ public class AudioDeviceInventory { mApmConnectedDevices.forEach((keyType, valueAddress) -> { pw.println(" " + prefix + " type:0x" + Integer.toHexString(keyType) + " (" + AudioSystem.getDeviceName(keyType) - + ") addr:" + valueAddress); }); + + ") addr:" + Utils.anonymizeBluetoothAddress(keyType, valueAddress)); }); pw.println("\n" + prefix + "Preferred devices for capture preset:"); mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> { pw.println(" " + prefix + "capturePreset:" + capturePreset @@ -1789,7 +1790,8 @@ public class AudioDeviceInventory { // TODO: return; } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "A2DP device addr=" + address + " now available").printLog(TAG)); + "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + + " now available").printLog(TAG)); } // Reset A2DP suspend state each time a new sink is connected @@ -2027,7 +2029,8 @@ public class AudioDeviceInventory { .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) { // removing A2DP device not currently used by AudioPolicy, log but don't act on it AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( - "A2DP device " + address + " made unavailable, was not used")).printLog(TAG)); + "A2DP device " + Utils.anonymizeBluetoothAddress(address) + + " made unavailable, was not used")).printLog(TAG)); mmi.set(MediaMetrics.Property.EARLY_RETURN, "A2DP device made unavailable, was not used") .record(); @@ -2043,13 +2046,15 @@ public class AudioDeviceInventory { if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM failed to make unavailable A2DP device addr=" + address + "APM failed to make unavailable A2DP device addr=" + + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printLog(TAG)); // TODO: failed to disconnect, stop here // TODO: return; } else { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( - "A2DP device addr=" + address + " made unavailable")).printLog(TAG)); + "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + + " made unavailable")).printLog(TAG)); } mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); @@ -2238,7 +2243,8 @@ public class AudioDeviceInventory { // TODO: return; } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "LE Audio device addr=" + address + " now available").printLog(TAG)); + "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) + + " now available").printLog(TAG)); } // Reset LEA suspend state each time a new sink is connected mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */); @@ -2282,7 +2288,8 @@ public class AudioDeviceInventory { // TODO: return; } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "LE Audio device addr=" + address + " made unavailable").printLog(TAG)); + "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) + + " made unavailable").printLog(TAG)); } mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address)); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index b2ee6101efcd..1e38c0f25157 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -136,6 +136,7 @@ import android.media.MediaMetrics; import android.media.MediaRecorder.AudioSource; import android.media.PlayerBase; import android.media.Spatializer; +import android.media.Utils; import android.media.VolumeInfo; import android.media.VolumePolicy; import android.media.audiofx.AudioEffect; @@ -7470,7 +7471,7 @@ public class AudioService extends IAudioService.Stub sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:" + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:" - + device.getAddress() + " behavior:" + + Utils.anonymizeBluetoothAddress(device.getAddress()) + " behavior:" + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior) + " pack:" + pkgName).printLog(TAG)); if (pkgName == null) { @@ -9641,7 +9642,7 @@ public class AudioService extends IAudioService.Stub private void avrcpSupportsAbsoluteVolume(String address, boolean support) { // address is not used for now, but may be used when multiple a2dp devices are supported sVolumeLogger.enqueue(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr=" - + address + " support=" + support).printLog(TAG)); + + Utils.anonymizeBluetoothAddress(address) + " support=" + support).printLog(TAG)); mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support); setAvrcpAbsoluteVolumeSupported(support); } @@ -10539,11 +10540,11 @@ public class AudioService extends IAudioService.Stub AudioDeviceAttributes retrieveBluetoothAddressUncheked(@NonNull AudioDeviceAttributes ada) { Objects.requireNonNull(ada); if (AudioSystem.isBluetoothDevice(ada.getInternalType())) { - String anonymizedAddress = anonymizeBluetoothAddress(ada.getAddress()); + String anonymizedAddress = Utils.anonymizeBluetoothAddress(ada.getAddress()); for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) { if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType()) && (ada.getInternalType() == ads.getInternalDeviceType()) - && anonymizedAddress.equals(anonymizeBluetoothAddress( + && anonymizedAddress.equals(Utils.anonymizeBluetoothAddress( ads.getDeviceAddress())))) { continue; } @@ -10554,19 +10555,6 @@ public class AudioService extends IAudioService.Stub return ada; } - /** - * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app - * Must match the implementation of BluetoothUtils.toAnonymizedAddress() - * @param address Mac address to be anonymized - * @return anonymized mac address - */ - static String anonymizeBluetoothAddress(String address) { - if (address == null || address.length() != "AA:BB:CC:DD:EE:FF".length()) { - return null; - } - return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length()); - } - private List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesList( List<AudioDeviceAttributes> devices) { if (isBluetoothPrividged()) { @@ -10590,7 +10578,7 @@ public class AudioService extends IAudioService.Stub return ada; } AudioDeviceAttributes res = new AudioDeviceAttributes(ada); - res.setAddress(anonymizeBluetoothAddress(ada.getAddress())); + res.setAddress(Utils.anonymizeBluetoothAddress(ada.getAddress())); return res; } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 53fbe8f37046..a12243b8e4fa 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -22,7 +22,6 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; -import static android.net.RouteInfo.RTN_THROW; import static android.net.RouteInfo.RTN_UNREACHABLE; import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN; import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO; @@ -45,12 +44,10 @@ import android.app.AppOpsManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -113,7 +110,6 @@ import android.os.Binder; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.CancellationSignal; -import android.os.FileUtils; import android.os.Handler; import android.os.IBinder; import android.os.INetworkManagementService; @@ -152,7 +148,6 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; -import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BinderUtils; import com.android.net.module.util.LinkPropertiesUtils; import com.android.net.module.util.NetdUtils; @@ -202,7 +197,6 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; /** * @hide @@ -1063,8 +1057,6 @@ public class Vpn { // Store mPackage since it might be reset or might be replaced with the other VPN app. final String oldPackage = mPackage; final boolean isPackageChanged = !Objects.equals(packageName, oldPackage); - // TODO: Remove "SdkLevel.isAtLeastT()" check once VpnManagerService is decoupled from - // ConnectivityServiceTest. // Only notify VPN apps that were already always-on, and only if the always-on provider // changed, or the lockdown mode changed. final boolean shouldNotifyOldPkg = isVpnApp(oldPackage) && mAlwaysOn @@ -1078,12 +1070,6 @@ public class Vpn { saveAlwaysOnPackage(); - // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from - // ConnectivityServiceTest. - if (!SdkLevel.isAtLeastT()) { - return true; - } - if (shouldNotifyOldPkg) { // If both of shouldNotifyOldPkg & isPackageChanged are true, that means the // always-on of old package is disabled or the old package is replaced with the new @@ -1984,9 +1970,7 @@ public class Vpn { for (String app : packageNames) { int uid = getAppUid(mContext, app, userId); if (uid != -1) uids.add(uid); - // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from - // ConnectivityServiceTest. - if (Process.isApplicationUid(uid) && SdkLevel.isAtLeastT()) { + if (Process.isApplicationUid(uid)) { uids.add(Process.toSdkSandboxUid(uid)); } } @@ -2297,15 +2281,6 @@ public class Vpn { private INetworkManagementEventObserver mObserver = new BaseNetworkObserver() { @Override - public void interfaceStatusChanged(String interfaze, boolean up) { - synchronized (Vpn.this) { - if (!up && mVpnRunner != null && mVpnRunner instanceof LegacyVpnRunner) { - ((LegacyVpnRunner) mVpnRunner).exitIfOuterInterfaceIs(interfaze); - } - } - } - - @Override public void interfaceRemoved(String interfaze) { synchronized (Vpn.this) { if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) { @@ -2556,17 +2531,6 @@ public class Vpn { private native boolean jniAddAddress(String interfaze, String address, int prefixLen); private native boolean jniDelAddress(String interfaze, String address, int prefixLen); - private static RouteInfo findIPv4DefaultRoute(LinkProperties prop) { - for (RouteInfo route : prop.getAllRoutes()) { - // Currently legacy VPN only works on IPv4. - if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) { - return route; - } - } - - throw new IllegalStateException("Unable to find IPv4 default gateway"); - } - private void enforceNotRestrictedUser() { final long token = Binder.clearCallingIdentity(); try { @@ -2665,10 +2629,6 @@ public class Vpn { throw new SecurityException("Restricted users cannot establish VPNs"); } - final RouteInfo ipv4DefaultRoute = findIPv4DefaultRoute(egress); - final String gateway = ipv4DefaultRoute.getGateway().getHostAddress(); - final String iface = ipv4DefaultRoute.getInterface(); - // Load certificates. String privateKey = ""; String userCert = ""; @@ -2700,8 +2660,6 @@ public class Vpn { throw new IllegalStateException("Cannot load credentials"); } - // Prepare arguments for racoon. - String[] racoon = null; switch (profile.type) { case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // Secret key is still just the alias (not the actual private key). The private key @@ -2731,109 +2689,9 @@ public class Vpn { // profile. startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN); return; - case VpnProfile.TYPE_L2TP_IPSEC_PSK: - racoon = new String[] { - iface, profile.server, "udppsk", profile.ipsecIdentifier, - profile.ipsecSecret, "1701", - }; - break; - case VpnProfile.TYPE_L2TP_IPSEC_RSA: - racoon = new String[] { - iface, profile.server, "udprsa", makeKeystoreEngineGrantString(privateKey), - userCert, caCert, serverCert, "1701", - }; - break; - case VpnProfile.TYPE_IPSEC_XAUTH_PSK: - racoon = new String[] { - iface, profile.server, "xauthpsk", profile.ipsecIdentifier, - profile.ipsecSecret, profile.username, profile.password, "", gateway, - }; - break; - case VpnProfile.TYPE_IPSEC_XAUTH_RSA: - racoon = new String[] { - iface, profile.server, "xauthrsa", makeKeystoreEngineGrantString(privateKey), - userCert, caCert, serverCert, profile.username, profile.password, "", gateway, - }; - break; - case VpnProfile.TYPE_IPSEC_HYBRID_RSA: - racoon = new String[] { - iface, profile.server, "hybridrsa", - caCert, serverCert, profile.username, profile.password, "", gateway, - }; - break; - } - - // Prepare arguments for mtpd. MTU/MRU calculated conservatively. Only IPv4 supported - // because LegacyVpn. - // 1500 - 60 (Carrier-internal IPv6 + UDP + GTP) - 10 (PPP) - 16 (L2TP) - 8 (UDP) - // - 77 (IPsec w/ SHA-2 512, 256b trunc-len, AES-CBC) - 8 (UDP encap) - 20 (IPv4) - // - 28 (464xlat) - String[] mtpd = null; - switch (profile.type) { - case VpnProfile.TYPE_PPTP: - mtpd = new String[] { - iface, "pptp", profile.server, "1723", - "name", profile.username, "password", profile.password, - "linkname", "vpn", "refuse-eap", "nodefaultroute", - "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270", - (profile.mppe ? "+mppe" : "nomppe"), - }; - if (profile.mppe) { - // Disallow PAP authentication when MPPE is requested, as MPPE cannot work - // with PAP anyway, and users may not expect PAP (plain text) to be used when - // MPPE was requested. - mtpd = Arrays.copyOf(mtpd, mtpd.length + 1); - mtpd[mtpd.length - 1] = "-pap"; - } - break; - case VpnProfile.TYPE_L2TP_IPSEC_PSK: - case VpnProfile.TYPE_L2TP_IPSEC_RSA: - mtpd = new String[] { - iface, "l2tp", profile.server, "1701", profile.l2tpSecret, - "name", profile.username, "password", profile.password, - "linkname", "vpn", "refuse-eap", "nodefaultroute", - "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270", - }; - break; } - VpnConfig config = new VpnConfig(); - config.legacy = true; - config.user = profile.key; - config.interfaze = iface; - config.session = profile.name; - config.isMetered = false; - config.proxyInfo = profile.proxy; - if (underlying != null) { - config.underlyingNetworks = new Network[] { underlying }; - } - - config.addLegacyRoutes(profile.routes); - if (!profile.dnsServers.isEmpty()) { - config.dnsServers = Arrays.asList(profile.dnsServers.split(" +")); - } - if (!profile.searchDomains.isEmpty()) { - config.searchDomains = Arrays.asList(profile.searchDomains.split(" +")); - } - startLegacyVpn(config, racoon, mtpd, profile); - } - - private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd, - VpnProfile profile) { - stopVpnRunnerPrivileged(); - - // Prepare for the new request. - prepareInternal(VpnConfig.LEGACY_VPN); - updateState(DetailedState.CONNECTING, "startLegacyVpn"); - - // Start a new LegacyVpnRunner and we are done! - mVpnRunner = new LegacyVpnRunner(config, racoon, mtpd, profile); - startLegacyVpnRunner(); - } - - @VisibleForTesting - protected void startLegacyVpnRunner() { - mVpnRunner.start(); + throw new UnsupportedOperationException("Legacy VPN is deprecated"); } /** @@ -2851,17 +2709,7 @@ public class Vpn { return; } - final boolean isLegacyVpn = mVpnRunner instanceof LegacyVpnRunner; mVpnRunner.exit(); - - // LegacyVpn uses daemons that must be shut down before new ones are brought up. - // The same limitation does not apply to Platform VPNs. - if (isLegacyVpn) { - synchronized (LegacyVpnRunner.TAG) { - // wait for old thread to completely finish before spinning up - // new instance, otherwise state updates can be out of order. - } - } } /** @@ -4143,9 +3991,7 @@ public class Vpn { // Ignore stale runner. if (mVpnRunner != this) return; - // TODO(b/230548427): Remove SDK check once VPN related stuff are - // decoupled from ConnectivityServiceTest. - if (SdkLevel.isAtLeastT() && category != null && isVpnApp(mPackage)) { + if (category != null && isVpnApp(mPackage)) { sendEventToVpnManagerApp(category, errorClass, errorCode, getPackage(), mSessionKey, makeVpnProfileStateLocked(), mActiveNetwork, @@ -4256,343 +4102,6 @@ public class Vpn { } } - /** - * Bringing up a VPN connection takes time, and that is all this thread - * does. Here we have plenty of time. The only thing we need to take - * care of is responding to interruptions as soon as possible. Otherwise - * requests will pile up. This could be done in a Handler as a state - * machine, but it is much easier to read in the current form. - */ - private class LegacyVpnRunner extends VpnRunner { - private static final String TAG = "LegacyVpnRunner"; - - private final String[] mDaemons; - private final String[][] mArguments; - private final LocalSocket[] mSockets; - private final String mOuterInterface; - private final AtomicInteger mOuterConnection = - new AtomicInteger(ConnectivityManager.TYPE_NONE); - private final VpnProfile mProfile; - - private long mBringupStartTime = -1; - - /** - * Watch for the outer connection (passing in the constructor) going away. - */ - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (!mEnableTeardown) return; - - if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { - if (intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, - ConnectivityManager.TYPE_NONE) == mOuterConnection.get()) { - NetworkInfo info = (NetworkInfo)intent.getExtra( - ConnectivityManager.EXTRA_NETWORK_INFO); - if (info != null && !info.isConnectedOrConnecting()) { - try { - mObserver.interfaceStatusChanged(mOuterInterface, false); - } catch (RemoteException e) {} - } - } - } - } - }; - - // GuardedBy("Vpn.this") (annotation can't be applied to constructor) - LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd, VpnProfile profile) { - super(TAG); - if (racoon == null && mtpd == null) { - throw new IllegalArgumentException( - "Arguments to racoon and mtpd must not both be null"); - } - mConfig = config; - mDaemons = new String[] {"racoon", "mtpd"}; - // TODO: clear arguments from memory once launched - mArguments = new String[][] {racoon, mtpd}; - mSockets = new LocalSocket[mDaemons.length]; - - // This is the interface which VPN is running on, - // mConfig.interfaze will change to point to OUR - // internal interface soon. TODO - add inner/outer to mconfig - // TODO - we have a race - if the outer iface goes away/disconnects before we hit this - // we will leave the VPN up. We should check that it's still there/connected after - // registering - mOuterInterface = mConfig.interfaze; - - mProfile = profile; - - if (!TextUtils.isEmpty(mOuterInterface)) { - for (Network network : mConnectivityManager.getAllNetworks()) { - final LinkProperties lp = mConnectivityManager.getLinkProperties(network); - if (lp != null && lp.getAllInterfaceNames().contains(mOuterInterface)) { - final NetworkInfo netInfo = mConnectivityManager.getNetworkInfo(network); - if (netInfo != null) { - mOuterConnection.set(netInfo.getType()); - break; - } - } - } - } - - IntentFilter filter = new IntentFilter(); - filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - mContext.registerReceiver(mBroadcastReceiver, filter); - } - - /** - * Checks if the parameter matches the underlying interface - * - * <p>If the underlying interface is torn down, the LegacyVpnRunner also should be. It has - * no ability to migrate between interfaces (or Networks). - */ - public void exitIfOuterInterfaceIs(String interfaze) { - if (interfaze.equals(mOuterInterface)) { - Log.i(TAG, "Legacy VPN is going down with " + interfaze); - exitVpnRunner(); - } - } - - /** Tears down this LegacyVpn connection */ - @Override - public void exitVpnRunner() { - // We assume that everything is reset after stopping the daemons. - interrupt(); - - // Always disconnect. This may be called again in cleanupVpnStateLocked() if - // exitVpnRunner() was called from exit(), but it will be a no-op. - agentDisconnect(); - try { - mContext.unregisterReceiver(mBroadcastReceiver); - } catch (IllegalArgumentException e) {} - } - - @Override - public void run() { - // Wait for the previous thread since it has been interrupted. - Log.v(TAG, "Waiting"); - synchronized (TAG) { - Log.v(TAG, "Executing"); - try { - bringup(); - waitForDaemonsToStop(); - interrupted(); // Clear interrupt flag if execute called exit. - } catch (InterruptedException e) { - } finally { - for (LocalSocket socket : mSockets) { - IoUtils.closeQuietly(socket); - } - // This sleep is necessary for racoon to successfully complete sending delete - // message to server. - try { - Thread.sleep(50); - } catch (InterruptedException e) { - } - for (String daemon : mDaemons) { - mDeps.stopService(daemon); - } - } - agentDisconnect(); - } - } - - private void checkInterruptAndDelay(boolean sleepLonger) throws InterruptedException { - long now = SystemClock.elapsedRealtime(); - if (now - mBringupStartTime <= 60000) { - Thread.sleep(sleepLonger ? 200 : 1); - } else { - updateState(DetailedState.FAILED, "checkpoint"); - throw new IllegalStateException("VPN bringup took too long"); - } - } - - private void checkAndFixupArguments(@NonNull final InetAddress endpointAddress) { - final String endpointAddressString = endpointAddress.getHostAddress(); - // Perform some safety checks before inserting the address in place. - // Position 0 in mDaemons and mArguments must be racoon, and position 1 must be mtpd. - if (!"racoon".equals(mDaemons[0]) || !"mtpd".equals(mDaemons[1])) { - throw new IllegalStateException("Unexpected daemons order"); - } - - // Respectively, the positions at which racoon and mtpd take the server address - // argument are 1 and 2. Not all types of VPN require both daemons however, and - // in that case the corresponding argument array is null. - if (mArguments[0] != null) { - if (!mProfile.server.equals(mArguments[0][1])) { - throw new IllegalStateException("Invalid server argument for racoon"); - } - mArguments[0][1] = endpointAddressString; - } - - if (mArguments[1] != null) { - if (!mProfile.server.equals(mArguments[1][2])) { - throw new IllegalStateException("Invalid server argument for mtpd"); - } - mArguments[1][2] = endpointAddressString; - } - } - - private void bringup() { - // Catch all exceptions so we can clean up a few things. - try { - // resolve never returns null. If it does because of some bug, it will be - // caught by the catch() block below and cleanup gracefully. - final InetAddress endpointAddress = mDeps.resolve(mProfile.server); - - // Big hack : dynamically replace the address of the server in the arguments - // with the resolved address. - checkAndFixupArguments(endpointAddress); - - // Initialize the timer. - mBringupStartTime = SystemClock.elapsedRealtime(); - - // Wait for the daemons to stop. - for (String daemon : mDaemons) { - while (!mDeps.isServiceStopped(daemon)) { - checkInterruptAndDelay(true); - } - } - - // Clear the previous state. - final File state = mDeps.getStateFile(); - state.delete(); - if (state.exists()) { - throw new IllegalStateException("Cannot delete the state"); - } - new File("/data/misc/vpn/abort").delete(); - - updateState(DetailedState.CONNECTING, "execute"); - - // Start the daemon with arguments. - for (int i = 0; i < mDaemons.length; ++i) { - String[] arguments = mArguments[i]; - if (arguments == null) { - continue; - } - - // Start the daemon. - String daemon = mDaemons[i]; - mDeps.startService(daemon); - - // Wait for the daemon to start. - while (!mDeps.isServiceRunning(daemon)) { - checkInterruptAndDelay(true); - } - - // Create the control socket. - mSockets[i] = new LocalSocket(); - - // Wait for the socket to connect and send over the arguments. - mDeps.sendArgumentsToDaemon(daemon, mSockets[i], arguments, - this::checkInterruptAndDelay); - } - - // Wait for the daemons to create the new state. - while (!state.exists()) { - // Check if a running daemon is dead. - for (int i = 0; i < mDaemons.length; ++i) { - String daemon = mDaemons[i]; - if (mArguments[i] != null && !mDeps.isServiceRunning(daemon)) { - throw new IllegalStateException(daemon + " is dead"); - } - } - checkInterruptAndDelay(true); - } - - // Now we are connected. Read and parse the new state. - String[] parameters = FileUtils.readTextFile(state, 0, null).split("\n", -1); - if (parameters.length != 7) { - throw new IllegalStateException("Cannot parse the state: '" - + String.join("', '", parameters) + "'"); - } - - // Set the interface and the addresses in the config. - synchronized (Vpn.this) { - mConfig.interfaze = parameters[0].trim(); - - mConfig.addLegacyAddresses(parameters[1]); - // Set the routes if they are not set in the config. - if (mConfig.routes == null || mConfig.routes.isEmpty()) { - mConfig.addLegacyRoutes(parameters[2]); - } - - // Set the DNS servers if they are not set in the config. - if (mConfig.dnsServers == null || mConfig.dnsServers.size() == 0) { - String dnsServers = parameters[3].trim(); - if (!dnsServers.isEmpty()) { - mConfig.dnsServers = Arrays.asList(dnsServers.split(" ")); - } - } - - // Set the search domains if they are not set in the config. - if (mConfig.searchDomains == null || mConfig.searchDomains.size() == 0) { - String searchDomains = parameters[4].trim(); - if (!searchDomains.isEmpty()) { - mConfig.searchDomains = Arrays.asList(searchDomains.split(" ")); - } - } - - // Add a throw route for the VPN server endpoint, if one was specified. - if (endpointAddress instanceof Inet4Address) { - mConfig.routes.add(new RouteInfo( - new IpPrefix(endpointAddress, 32), null /*gateway*/, - null /*iface*/, RTN_THROW)); - } else if (endpointAddress instanceof Inet6Address) { - mConfig.routes.add(new RouteInfo( - new IpPrefix(endpointAddress, 128), null /*gateway*/, - null /*iface*/, RTN_THROW)); - } else { - Log.e(TAG, "Unknown IP address family for VPN endpoint: " - + endpointAddress); - } - - // Here is the last step and it must be done synchronously. - // Set the start time - mConfig.startTime = SystemClock.elapsedRealtime(); - - // Check if the thread was interrupted while we were waiting on the lock. - checkInterruptAndDelay(false); - - // Check if the interface is gone while we are waiting. - if (!mDeps.isInterfacePresent(Vpn.this, mConfig.interfaze)) { - throw new IllegalStateException(mConfig.interfaze + " is gone"); - } - - // Now INetworkManagementEventObserver is watching our back. - mInterface = mConfig.interfaze; - prepareStatusIntent(); - - agentConnect(); - - Log.i(TAG, "Connected!"); - } - } catch (Exception e) { - Log.i(TAG, "Aborting", e); - updateState(DetailedState.FAILED, e.getMessage()); - exitVpnRunner(); - } - } - - /** - * Check all daemons every two seconds. Return when one of them is stopped. - * The caller will move to the disconnected state when this function returns, - * which can happen if a daemon failed or if the VPN was torn down. - */ - private void waitForDaemonsToStop() throws InterruptedException { - if (!mNetworkInfo.isConnected()) { - return; - } - while (true) { - Thread.sleep(2000); - for (int i = 0; i < mDaemons.length; i++) { - if (mArguments[i] != null && mDeps.isServiceStopped(mDaemons[i])) { - return; - } - } - } - } - } - private void verifyCallingUidAndPackage(String packageName) { mDeps.verifyCallingUidAndPackage(mContext, packageName, mUserId); } @@ -4839,11 +4348,9 @@ public class Vpn { // Build intent first because the sessionKey will be reset after performing // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in // VpnRunner.exit() to prevent design being changed in the future. - // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from - // ConnectivityServiceTest. final int ownerUid = mOwnerUID; Intent intent = null; - if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) { + if (isVpnApp(mPackage)) { intent = buildVpnManagerEventIntent( VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER, -1 /* errorClass */, -1 /* errorCode*/, mPackage, @@ -4884,12 +4391,8 @@ public class Vpn { // The underlying network, NetworkCapabilities and LinkProperties are not // necessary to send to VPN app since the purpose of this event is to notify // VPN app that VPN is deactivated by the user. - // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from - // ConnectivityServiceTest. - if (SdkLevel.isAtLeastT()) { - mEventChanges.log("[VMEvent] " + packageName + " stopped"); - sendEventToVpnManagerApp(intent, packageName); - } + mEventChanges.log("[VMEvent] " + packageName + " stopped"); + sendEventToVpnManagerApp(intent, packageName); } private boolean storeAppExclusionList(@NonNull String packageName, diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 1bdd402cf0b5..707e99019df3 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -179,6 +179,8 @@ import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; +import android.companion.AssociationInfo; +import android.companion.AssociationRequest; import android.companion.ICompanionDeviceManager; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -546,6 +548,15 @@ public class NotificationManagerService extends SystemService { @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) static final long ENFORCE_NO_CLEAR_FLAG_ON_MEDIA_NOTIFICATION = 264179692L; + /** + * App calls to {@link android.app.NotificationManager#setInterruptionFilter} and + * {@link android.app.NotificationManager#setNotificationPolicy} manage DND through the + * creation and activation of an implicit {@link android.app.AutomaticZenRule}. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + static final long MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES = 308670109L; + private static final Duration POST_WAKE_LOCK_TIMEOUT = Duration.ofSeconds(30); private IActivityManager mAm; @@ -5343,6 +5354,12 @@ public class NotificationManagerService extends SystemService { if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter); final int callingUid = Binder.getCallingUid(); final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); + + if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) { + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen); + return; + } + final long identity = Binder.clearCallingIdentity(); try { mZenModeHelper.setManualZenMode(zen, null, pkg, "setInterruptionFilter", @@ -5426,6 +5443,16 @@ public class NotificationManagerService extends SystemService { } } + private boolean canManageGlobalZenPolicy(String callingPkg, int callingUid) { + boolean isCompatChangeEnabled = Binder.withCleanCallingIdentity( + () -> CompatChanges.isChangeEnabled(MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES, + callingUid)); + return !isCompatChangeEnabled + || isCallerIsSystemOrSystemUi() + || hasCompanionDevice(callingPkg, UserHandle.getUserId(callingUid), + AssociationRequest.DEVICE_PROFILE_WATCH); + } + private void enforcePolicyAccess(String pkg, String method) { if (PackageManager.PERMISSION_GRANTED == getContext().checkCallingPermission( android.Manifest.permission.MANAGE_NOTIFICATIONS)) { @@ -5619,6 +5646,10 @@ public class NotificationManagerService extends SystemService { @Override public Policy getNotificationPolicy(String pkg) { + final int callingUid = Binder.getCallingUid(); + if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) { + return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(pkg); + } final long identity = Binder.clearCallingIdentity(); try { return mZenModeHelper.getNotificationPolicy(); @@ -5649,6 +5680,10 @@ public class NotificationManagerService extends SystemService { enforcePolicyAccess(pkg, "setNotificationPolicy"); int callingUid = Binder.getCallingUid(); boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); + + boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi() + && !canManageGlobalZenPolicy(pkg, callingUid); + final long identity = Binder.clearCallingIdentity(); try { final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkg, @@ -5687,16 +5722,21 @@ public class NotificationManagerService extends SystemService { policy = new Policy(policy.priorityCategories, policy.priorityCallSenders, policy.priorityMessageSenders, newVisualEffects, policy.priorityConversationSenders); - ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy); - mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi); + + if (shouldApplyAsImplicitRule) { + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy); + } else { + ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, + policy); + mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi); + } } catch (RemoteException e) { + Slog.e(TAG, "Failed to set notification policy", e); } finally { Binder.restoreCallingIdentity(identity); } } - - @Override public List<String> getEnabledNotificationListenerPackages() { checkCallerIsSystem(); @@ -10556,6 +10596,12 @@ public class NotificationManagerService extends SystemService { } boolean hasCompanionDevice(ManagedServiceInfo info) { + return hasCompanionDevice(info.component.getPackageName(), + info.userid, /* withDeviceProfile= */ null); + } + + private boolean hasCompanionDevice(String pkg, @UserIdInt int userId, + @Nullable @AssociationRequest.DeviceProfile String withDeviceProfile) { if (mCompanionManager == null) { mCompanionManager = getCompanionManager(); } @@ -10565,17 +10611,19 @@ public class NotificationManagerService extends SystemService { } final long identity = Binder.clearCallingIdentity(); try { - List<?> associations = mCompanionManager.getAssociations( - info.component.getPackageName(), info.userid); - if (!ArrayUtils.isEmpty(associations)) { - return true; + List<AssociationInfo> associations = mCompanionManager.getAssociations(pkg, userId); + for (AssociationInfo association : associations) { + if (withDeviceProfile == null || withDeviceProfile.equals( + association.getDeviceProfile())) { + return true; + } } } catch (SecurityException se) { // Not a privileged listener } catch (RemoteException re) { Slog.e(TAG, "Cannot reach companion device service", re); } catch (Exception e) { - Slog.e(TAG, "Cannot verify listener " + info, e); + Slog.e(TAG, "Cannot verify caller pkg=" + pkg + ", userId=" + userId, e); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java index 8f5676b31594..9106c33d26db 100644 --- a/services/core/java/com/android/server/notification/SnoozeHelper.java +++ b/services/core/java/com/android/server/notification/SnoozeHelper.java @@ -118,7 +118,10 @@ public final class SnoozeHelper { protected boolean canSnooze(int numberToSnooze) { synchronized (mLock) { - if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) { + if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT + || (mPersistedSnoozedNotifications.size() + + mPersistedSnoozedNotificationsWithContext.size() + numberToSnooze) + > CONCURRENT_SNOOZE_LIMIT) { return false; } } @@ -357,6 +360,9 @@ public final class SnoozeHelper { if (groupSummaryKey != null) { NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey); + String trimmedKey = getTrimmedString(groupSummaryKey); + mPersistedSnoozedNotificationsWithContext.remove(trimmedKey); + mPersistedSnoozedNotifications.remove(trimmedKey); if (record != null && !record.isCanceled) { Runnable runnable = () -> { diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java new file mode 100644 index 000000000000..2a65aff7f28d --- /dev/null +++ b/services/core/java/com/android/server/notification/ZenAdapters.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import android.app.NotificationManager.Policy; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenPolicy; + +/** + * Converters between different Zen representations. + */ +class ZenAdapters { + + static ZenPolicy notificationPolicyToZenPolicy(Policy policy) { + ZenPolicy.Builder zenPolicyBuilder = new ZenPolicy.Builder() + .allowAlarms(policy.allowAlarms()) + .allowCalls( + policy.allowCalls() + ? ZenModeConfig.getZenPolicySenders(policy.allowCallsFrom()) + : ZenPolicy.PEOPLE_TYPE_NONE) + .allowConversations( + policy.allowConversations() + ? notificationPolicyConversationSendersToZenPolicy( + policy.allowConversationsFrom()) + : ZenPolicy.CONVERSATION_SENDERS_NONE) + .allowEvents(policy.allowEvents()) + .allowMedia(policy.allowMedia()) + .allowMessages( + policy.allowMessages() + ? ZenModeConfig.getZenPolicySenders(policy.allowMessagesFrom()) + : ZenPolicy.PEOPLE_TYPE_NONE) + .allowReminders(policy.allowReminders()) + .allowRepeatCallers(policy.allowRepeatCallers()) + .allowSystem(policy.allowSystem()); + + if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) { + zenPolicyBuilder.showBadges(policy.showBadges()) + .showFullScreenIntent(policy.showFullScreenIntents()) + .showInAmbientDisplay(policy.showAmbient()) + .showInNotificationList(policy.showInNotificationList()) + .showLights(policy.showLights()) + .showPeeking(policy.showPeeking()) + .showStatusBarIcons(policy.showStatusBarIcons()); + } + + return zenPolicyBuilder.build(); + } + + @ZenPolicy.ConversationSenders + private static int notificationPolicyConversationSendersToZenPolicy( + int npPriorityConversationSenders) { + switch (npPriorityConversationSenders) { + case Policy.CONVERSATION_SENDERS_ANYONE: + return ZenPolicy.CONVERSATION_SENDERS_ANYONE; + case Policy.CONVERSATION_SENDERS_IMPORTANT: + return ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; + case Policy.CONVERSATION_SENDERS_NONE: + return ZenPolicy.CONVERSATION_SENDERS_NONE; + case Policy.CONVERSATION_SENDERS_UNSET: + default: + return ZenPolicy.CONVERSATION_SENDERS_UNSET; + } + } +} diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 762c1a162302..c637df2ec99b 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -27,6 +27,7 @@ import static android.service.notification.NotificationServiceProto.ROOT_CONFIG; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.AppOpsManager; @@ -44,6 +45,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -349,11 +351,11 @@ public class ZenModeHelper { ZenRule rule; synchronized (mConfigLock) { if (mConfig == null) return null; - rule = mConfig.automaticRules.get(id); + rule = mConfig.automaticRules.get(id); } if (rule == null) return null; if (canManageAutomaticZenRule(rule)) { - return createAutomaticZenRule(rule); + return zenRuleToAutomaticZenRule(rule); } return null; } @@ -439,6 +441,167 @@ public class ZenModeHelper { } } + /** + * Create (or activate, or deactivate) an "implicit" {@link ZenRule} when an app that has + * Notification Policy Access but is not allowed to manage the global zen state + * calls {@link NotificationManager#setInterruptionFilter}. + * + * <p>When the {@code zenMode} is {@link Global#ZEN_MODE_OFF}, an existing implicit rule will be + * deactivated (if there is no implicit rule, the call will be ignored). For other modes, the + * rule's interruption filter will match the supplied {@code zenMode}. The policy of the last + * call to {@link NotificationManager#setNotificationPolicy} will be used (or, if never called, + * the global policy). + * + * <p>The created rule is owned by the calling package, but it has neither a + * {@link ConditionProviderService} nor an associated + * {@link AutomaticZenRule#configurationActivity}. + * + * @param zenMode one of the {@code Global#ZEN_MODE_x} values + */ + void applyGlobalZenModeAsImplicitZenRule(String callingPkg, int callingUid, int zenMode) { + if (!android.app.Flags.modesApi()) { + Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!"); + return; + } + synchronized (mConfigLock) { + if (mConfig == null) { + return; + } + if (zenMode == Global.ZEN_MODE_OFF) { + // Deactivate implicit rule if it exists and is active; otherwise ignore. + ZenRule rule = mConfig.automaticRules.get(implicitRuleId(callingPkg)); + if (rule != null) { + Condition deactivated = new Condition(rule.conditionId, + mContext.getString(R.string.zen_mode_implicit_deactivated), + Condition.STATE_FALSE); + setAutomaticZenRuleState(rule.id, deactivated, + callingUid, /* fromSystemOrSystemUi= */ false); + } + } else { + // Either create a new rule with a default ZenPolicy, or update an existing rule's + // filter value. In both cases, also activate (and unsnooze) it. + ZenModeConfig newConfig = mConfig.copy(); + ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); + if (rule == null) { + rule = newImplicitZenRule(callingPkg); + newConfig.automaticRules.put(rule.id, rule); + } + rule.zenMode = zenMode; + rule.snoozing = false; + rule.condition = new Condition(rule.conditionId, + mContext.getString(R.string.zen_mode_implicit_activated), + Condition.STATE_TRUE); + setConfigLocked(newConfig, /* triggeringComponent= */ null, + "applyGlobalZenModeAsImplicitZenRule", + callingUid, /* fromSystemOrSystemUi= */ false); + } + } + } + + /** + * Create (or update) an "implicit" {@link ZenRule} when an app that has Notification Policy + * Access but is not allowed to manage the global zen state calls + * {@link NotificationManager#setNotificationPolicy}. + * + * <p>The created rule is owned by the calling package and has the {@link ZenPolicy} + * corresponding to the supplied {@code policy}, but it has neither a + * {@link ConditionProviderService} nor an associated + * {@link AutomaticZenRule#configurationActivity}. Its zen mode will be set to + * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}. + */ + void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid, + NotificationManager.Policy policy) { + if (!android.app.Flags.modesApi()) { + Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!"); + return; + } + synchronized (mConfigLock) { + if (mConfig == null) { + return; + } + ZenModeConfig newConfig = mConfig.copy(); + ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); + if (rule == null) { + rule = newImplicitZenRule(callingPkg); + rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + newConfig.automaticRules.put(rule.id, rule); + } + // TODO: b/308673679 - Keep user customization of this rule! + rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy); + setConfigLocked(newConfig, /* triggeringComponent= */ null, + "applyGlobalPolicyAsImplicitZenRule", + callingUid, /* fromSystemOrSystemUi= */ false); + } + } + + /** + * Returns the {@link Policy} associated to the "implicit" {@link ZenRule} of a package that has + * Notification Policy Access but is not allowed to manage the global zen state. + * + * <p>If the implicit rule doesn't exist, or it doesn't specify a {@link ZenPolicy} (because the + * app never called {@link NotificationManager#setNotificationPolicy}) then the default policy + * is returned (i.e. same as {@link #getNotificationPolicy}. + * + * <p>Any unset values in the {@link ZenPolicy} will be mapped to their current defaults. + */ + @Nullable + Policy getNotificationPolicyFromImplicitZenRule(String callingPkg) { + if (!android.app.Flags.modesApi()) { + Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!"); + return getNotificationPolicy(); + } + synchronized (mConfigLock) { + if (mConfig == null) { + return null; + } + ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg)); + if (implicitRule != null && implicitRule.zenPolicy != null) { + return mConfig.toNotificationPolicy(implicitRule.zenPolicy); + } else { + return getNotificationPolicy(); + } + } + } + + /** + * Creates an empty {@link ZenRule} to be used as the implicit rule for {@code pkg}. + * Both {@link ZenRule#zenMode} and {@link ZenRule#zenPolicy} are unset. + */ + private ZenRule newImplicitZenRule(String pkg) { + ZenRule rule = new ZenRule(); + rule.id = implicitRuleId(pkg); + rule.pkg = pkg; + rule.creationTime = System.currentTimeMillis(); + + Binder.withCleanCallingIdentity(() -> { + try { + ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0); + rule.name = applicationInfo.loadLabel(mPm).toString(); + } catch (PackageManager.NameNotFoundException e) { + // Should not happen, since it's the app calling us (?) + Log.w(TAG, "Package not found for creating implicit zen rule"); + rule.name = "Unknown"; + } + }); + + rule.condition = null; + rule.conditionId = new Uri.Builder() + .scheme(Condition.SCHEME) + .authority("android") + .appendPath("implicit") + .appendPath(pkg) + .build(); + rule.enabled = true; + rule.modified = false; + rule.component = null; + rule.configurationActivity = null; + return rule; + } + + private static String implicitRuleId(String forPackage) { + return "implicit_" + forPackage; + } + public boolean removeAutomaticZenRule(String id, String reason, int callingUid, boolean fromSystemOrSystemUi) { ZenModeConfig newConfig; @@ -626,7 +789,7 @@ public class ZenModeHelper { } // update default rule (if locale changed, name of rule will change) currRule.name = defaultRule.name; - updateAutomaticZenRule(defaultRule.id, createAutomaticZenRule(currRule), + updateAutomaticZenRule(defaultRule.id, zenRuleToAutomaticZenRule(currRule), "locale changed", callingUid, fromSystemOrSystemUi); } } @@ -669,7 +832,7 @@ public class ZenModeHelper { return null; } - private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule, + private static void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule, boolean isNew) { if (rule.enabled != automaticZenRule.isEnabled()) { rule.snoozing = false; @@ -699,7 +862,7 @@ public class ZenModeHelper { } } - protected AutomaticZenRule createAutomaticZenRule(ZenRule rule) { + private static AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) { AutomaticZenRule azr; if (Flags.modesApi()) { azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId) diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 25b7ca146ff1..dcac8c98d19f 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -7,8 +7,6 @@ flag { bug: "290381858" } - - flag { name: "polite_notifications" namespace: "systemui" diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a7784152a883..c9703dbe6311 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -4648,7 +4648,7 @@ class Task extends TaskFragment { // Expanding pip into new rotation, so create a rotation leash // until the display is rotated. topActivity.getOrCreateFixedRotationLeash( - topActivity.getSyncTransaction()); + topActivity.getPendingTransaction()); } lastParentBeforePip.moveToFront("movePinnedActivityToOriginalTask"); } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index c0bf2ce41435..3e23fab86223 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1052,12 +1052,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { * @return true if we are *guaranteed* to enter-pip. This means we return false if there's * a chance we won't thus legacy-entry (via pause+userLeaving) will return false. */ - private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar, - @Nullable ActivityRecord resuming) { + private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar) { if (!mCanPipOnFinish || !ar.isVisible() || ar.getTask() == null || !ar.isState(RESUMED)) { return false; } + final ActivityRecord resuming = getVisibleTransientLaunch(ar.getTaskDisplayArea()); if (ar.pictureInPictureArgs != null && ar.pictureInPictureArgs.isAutoEnterEnabled()) { if (!ar.getTask().isVisibleRequested() || didCommitTransientLaunch()) { // force enable pip-on-task-switch now that we've committed to actually launching @@ -1196,9 +1196,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final boolean isScreenOff = ar.mDisplayContent == null || ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF; if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) { - final ActivityRecord resuming = getVisibleTransientLaunch( - ar.getTaskDisplayArea()); - final boolean commitVisibility = !checkEnterPipOnFinish(ar, resuming); + final boolean commitVisibility = !checkEnterPipOnFinish(ar); // Avoid commit visibility if entering pip or else we will get a sudden // "flash" / surface going invisible for a split second. if (commitVisibility) { @@ -1431,7 +1429,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (candidateActivity.getTaskDisplayArea() != taskDisplayArea) { continue; } - if (!candidateActivity.isVisible()) { + if (!candidateActivity.isVisibleRequested()) { continue; } return candidateActivity; diff --git a/services/core/java/com/android/server/wm/WindowManagerFlags.java b/services/core/java/com/android/server/wm/WindowManagerFlags.java index 46677107c670..b3a36501b7cf 100644 --- a/services/core/java/com/android/server/wm/WindowManagerFlags.java +++ b/services/core/java/com/android/server/wm/WindowManagerFlags.java @@ -49,5 +49,8 @@ class WindowManagerFlags { final boolean mWallpaperOffsetAsync = Flags.wallpaperOffsetAsync(); + final boolean mAllowsScreenSizeDecoupledFromStatusBarAndCutout = + Flags.allowsScreenSizeDecoupledFromStatusBarAndCutout(); + /* End Available Flags */ } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 55678c525c4b..0a986c8a6f2f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -336,7 +336,6 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; import com.android.server.utils.PriorityDump; -import com.android.window.flags.Flags; import dalvik.annotation.optimization.NeverCompile; @@ -1194,7 +1193,7 @@ public class WindowManagerService extends IWindowManager.Stub .getBoolean(R.bool.config_skipActivityRelaunchWhenDocking); final boolean isScreenSizeDecoupledFromStatusBarAndCutout = context.getResources() .getBoolean(R.bool.config_decoupleStatusBarAndDisplayCutoutFromScreenSize) - && Flags.closeToSquareConfigIncludesStatusBar(); + && mFlags.mAllowsScreenSizeDecoupledFromStatusBarAndCutout; if (!isScreenSizeDecoupledFromStatusBarAndCutout) { mDecorTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars(); mConfigTypes = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars(); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java new file mode 100644 index 000000000000..72dc7259dc1f --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.annotation.NonNull; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.TestLooperManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.SparseArray; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.AlarmManagerInternal; +import com.android.server.DropBoxManagerInternal; +import com.android.server.LocalServices; +import com.android.server.appop.AppOpsService; +import com.android.server.wm.ActivityTaskManagerService; + +import org.junit.Rule; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class BaseBroadcastQueueTest { + + static final int USER_GUEST = 11; + + static final String PACKAGE_ANDROID = "android"; + static final String PACKAGE_PHONE = "com.android.phone"; + static final String PACKAGE_RED = "com.example.red"; + static final String PACKAGE_GREEN = "com.example.green"; + static final String PACKAGE_BLUE = "com.example.blue"; + static final String PACKAGE_YELLOW = "com.example.yellow"; + static final String PACKAGE_ORANGE = "com.example.orange"; + + static final String PROCESS_SYSTEM = "system"; + + static final String CLASS_RED = "com.example.red.Red"; + static final String CLASS_GREEN = "com.example.green.Green"; + static final String CLASS_BLUE = "com.example.blue.Blue"; + static final String CLASS_YELLOW = "com.example.yellow.Yellow"; + static final String CLASS_ORANGE = "com.example.orange.Orange"; + + static final BroadcastProcessQueue.BroadcastPredicate BROADCAST_PREDICATE_ANY = + (r, i) -> true; + + @Rule + public final ApplicationExitInfoTest.ServiceThreadRule + mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule(); + + @Mock + AppOpsService mAppOpsService; + @Mock + PackageManagerInternal mPackageManagerInt; + @Mock + UsageStatsManagerInternal mUsageStatsManagerInt; + @Mock + DropBoxManagerInternal mDropBoxManagerInt; + @Mock + AlarmManagerInternal mAlarmManagerInt; + @Mock + ProcessList mProcessList; + + Context mContext; + ActivityManagerService mAms; + BroadcastConstants mConstants; + BroadcastSkipPolicy mSkipPolicy; + HandlerThread mHandlerThread; + TestLooperManager mLooper; + AtomicInteger mNextPid; + + /** + * Map from PID to registered registered runtime receivers. + */ + SparseArray<ReceiverList> mRegisteredReceivers = new SparseArray<>(); + + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mHandlerThread = new HandlerThread(getTag()); + mHandlerThread.start(); + // Pause all event processing until a test chooses to resume + mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation() + .acquireLooperManager(mHandlerThread.getLooper())); + mNextPid = new AtomicInteger(100); + + LocalServices.removeServiceForTest(DropBoxManagerInternal.class); + LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt); + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + LocalServices.removeServiceForTest(AlarmManagerInternal.class); + LocalServices.addService(AlarmManagerInternal.class, mAlarmManagerInt); + doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); + doNothing().when(mPackageManagerInt).notifyComponentUsed(any(), anyInt(), any(), any()); + doAnswer((invocation) -> { + return getUidForPackage(invocation.getArgument(0)); + }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM)); + + final ActivityManagerService realAms = new ActivityManagerService( + new TestInjector(mContext), mServiceThreadRule.getThread()); + realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); + realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); + realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal()); + realAms.mOomAdjuster = spy(realAms.mOomAdjuster); + realAms.mPackageManagerInt = mPackageManagerInt; + realAms.mUsageStatsService = mUsageStatsManagerInt; + realAms.mProcessesReady = true; + mAms = spy(realAms); + + mSkipPolicy = spy(new BroadcastSkipPolicy(mAms)); + doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any()); + doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any()); + + mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS); + } + + public void tearDown() throws Exception { + mHandlerThread.quit(); + } + + static int getUidForPackage(@NonNull String packageName) { + switch (packageName) { + case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID; + case PACKAGE_PHONE: return android.os.Process.PHONE_UID; + case PACKAGE_RED: return android.os.Process.FIRST_APPLICATION_UID + 1; + case PACKAGE_GREEN: return android.os.Process.FIRST_APPLICATION_UID + 2; + case PACKAGE_BLUE: return android.os.Process.FIRST_APPLICATION_UID + 3; + case PACKAGE_YELLOW: return android.os.Process.FIRST_APPLICATION_UID + 4; + case PACKAGE_ORANGE: return android.os.Process.FIRST_APPLICATION_UID + 5; + default: throw new IllegalArgumentException(); + } + } + + static int getUidForPackage(@NonNull String packageName, int userId) { + return UserHandle.getUid(userId, getUidForPackage(packageName)); + } + + private class TestInjector extends ActivityManagerService.Injector { + TestInjector(Context context) { + super(context); + } + + @Override + public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile, + Handler handler) { + return mAppOpsService; + } + + @Override + public Handler getUiHandler(ActivityManagerService service) { + return mHandlerThread.getThreadHandler(); + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mProcessList; + } + } + + abstract String getTag(); + + static ApplicationInfo makeApplicationInfo(String packageName) { + return makeApplicationInfo(packageName, packageName, UserHandle.USER_SYSTEM); + } + + static ApplicationInfo makeApplicationInfo(String packageName, String processName, int userId) { + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ai.processName = processName; + ai.uid = getUidForPackage(packageName, userId); + return ai; + } + + static ResolveInfo withPriority(ResolveInfo info, int priority) { + info.priority = priority; + return info; + } + + static BroadcastFilter withPriority(BroadcastFilter filter, int priority) { + filter.setPriority(priority); + return filter; + } + + static ResolveInfo makeManifestReceiver(String packageName, String name) { + return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM); + } + + static ResolveInfo makeManifestReceiver(String packageName, String name, int userId) { + return makeManifestReceiver(packageName, packageName, name, userId); + } + + static ResolveInfo makeManifestReceiver(String packageName, String processName, + String name, int userId) { + final ResolveInfo ri = new ResolveInfo(); + ri.activityInfo = new ActivityInfo(); + ri.activityInfo.packageName = packageName; + ri.activityInfo.processName = processName; + ri.activityInfo.name = name; + ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId); + return ri; + } + + BroadcastFilter makeRegisteredReceiver(ProcessRecord app) { + return makeRegisteredReceiver(app, 0); + } + + BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) { + final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid()); + return makeRegisteredReceiver(receiverList, priority); + } + + static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) { + final IntentFilter filter = new IntentFilter(); + filter.setPriority(priority); + final BroadcastFilter res = new BroadcastFilter(filter, receiverList, + receiverList.app.info.packageName, null, null, null, receiverList.uid, + receiverList.userId, false, false, true); + receiverList.add(res); + return res; + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 08f5d03e0148..2378416f8bd0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -31,17 +31,6 @@ import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_ORDERE import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_PRIORITIZED; import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList; import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList; -import static com.android.server.am.BroadcastQueueTest.CLASS_BLUE; -import static com.android.server.am.BroadcastQueueTest.CLASS_GREEN; -import static com.android.server.am.BroadcastQueueTest.CLASS_RED; -import static com.android.server.am.BroadcastQueueTest.CLASS_YELLOW; -import static com.android.server.am.BroadcastQueueTest.PACKAGE_BLUE; -import static com.android.server.am.BroadcastQueueTest.PACKAGE_GREEN; -import static com.android.server.am.BroadcastQueueTest.PACKAGE_RED; -import static com.android.server.am.BroadcastQueueTest.PACKAGE_YELLOW; -import static com.android.server.am.BroadcastQueueTest.getUidForPackage; -import static com.android.server.am.BroadcastQueueTest.makeManifestReceiver; -import static com.android.server.am.BroadcastQueueTest.withPriority; import static com.android.server.am.BroadcastRecord.isReceiverEquals; import static com.google.common.truth.Truth.assertThat; @@ -74,17 +63,15 @@ import android.appwidget.AppWidgetManager; import android.content.IIntentReceiver; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.media.AudioManager; import android.os.Bundle; import android.os.BundleMerger; import android.os.DropBoxManager; -import android.os.HandlerThread; import android.os.Process; import android.os.SystemClock; -import android.os.TestLooperManager; import android.os.UserHandle; -import android.provider.Settings; import android.util.IndentingPrintWriter; import android.util.Pair; @@ -108,11 +95,12 @@ import java.util.List; import java.util.Objects; @SmallTest -public final class BroadcastQueueModernImplTest { +public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { + private static final String TAG = "BroadcastQueueModernImplTest"; + private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID; private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1; - @Mock ActivityManagerService mAms; @Mock ProcessRecord mProcess; @Mock BroadcastProcessQueue mQueue1; @@ -120,11 +108,6 @@ public final class BroadcastQueueModernImplTest { @Mock BroadcastProcessQueue mQueue3; @Mock BroadcastProcessQueue mQueue4; - HandlerThread mHandlerThread; - TestLooperManager mLooper; - - BroadcastConstants mConstants; - private BroadcastSkipPolicy mSkipPolicy; BroadcastQueueModernImpl mImpl; BroadcastProcessQueue mHead; @@ -136,22 +119,12 @@ public final class BroadcastQueueModernImplTest { @Before public void setUp() throws Exception { - mHandlerThread = new HandlerThread(getClass().getSimpleName()); - mHandlerThread.start(); - - // Pause all event processing until a test chooses to resume - mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation() - .acquireLooperManager(mHandlerThread.getLooper())); + super.setUp(); - mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS); mConstants.DELAY_URGENT_MILLIS = -120_000; mConstants.DELAY_NORMAL_MILLIS = 10_000; mConstants.DELAY_CACHED_MILLIS = 120_000; - mSkipPolicy = spy(new BroadcastSkipPolicy(mAms)); - doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any()); - doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any()); - final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) { public void addBroadcastToHistoryLocked(BroadcastRecord original) { // Ignored @@ -169,7 +142,12 @@ public final class BroadcastQueueModernImplTest { @After public void tearDown() throws Exception { - mHandlerThread.quit(); + super.tearDown(); + } + + @Override + public String getTag() { + return TAG; } /** @@ -225,11 +203,6 @@ public final class BroadcastQueueModernImplTest { List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), false); } - private BroadcastRecord makeOrderedBroadcastRecord(Intent intent) { - return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(), - List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), true); - } - private BroadcastRecord makeBroadcastRecord(Intent intent, List receivers) { return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(), receivers, false); } @@ -246,8 +219,8 @@ public final class BroadcastQueueModernImplTest { private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options, List receivers, IIntentReceiver resultTo, boolean ordered) { - return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, 42, false, null, - null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo, + return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, TEST_UID, false, + null, null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo, Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM, BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN); } @@ -259,12 +232,12 @@ public final class BroadcastQueueModernImplTest { private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue, BroadcastRecord record, int recordIndex, long enqueueTime) { - queue.enqueueOrReplaceBroadcast(record, recordIndex, (r, i) -> { - throw new UnsupportedOperationException(); - }); record.enqueueTime = enqueueTime; record.enqueueRealTime = enqueueTime; record.enqueueClockTime = enqueueTime; + queue.enqueueOrReplaceBroadcast(record, recordIndex, (r, i) -> { + throw new UnsupportedOperationException(); + }); } @Test @@ -419,6 +392,7 @@ public final class BroadcastQueueModernImplTest { assertFalse(queue.isRunnable()); assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, queue.getRunnableAtReason()); + assertTrue(queue.shouldBeDeferred()); assertEquals(ProcessList.SCHED_GROUP_UNDEFINED, queue.getPreferredSchedulingGroupLocked()); } @@ -445,6 +419,7 @@ public final class BroadcastQueueModernImplTest { assertThat(cachedRunnableAt).isGreaterThan(notCachedRunnableAt); assertTrue(queue.isRunnable()); assertEquals(BroadcastProcessQueue.REASON_CACHED, queue.getRunnableAtReason()); + assertTrue(queue.shouldBeDeferred()); assertEquals(ProcessList.SCHED_GROUP_UNDEFINED, queue.getPreferredSchedulingGroupLocked()); } @@ -526,11 +501,13 @@ public final class BroadcastQueueModernImplTest { queue.invalidateRunnableAt(); assertThat(queue.getRunnableAt()).isGreaterThan(airplaneRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); mConstants.MAX_PENDING_BROADCASTS = 1; queue.invalidateRunnableAt(); assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_MAX_PENDING, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); } @Test @@ -549,10 +526,12 @@ public final class BroadcastQueueModernImplTest { queue.setProcessAndUidState(mProcess, true, false); assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_FOREGROUND, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); queue.setProcessAndUidState(mProcess, false, false); assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); } @Test @@ -570,6 +549,7 @@ public final class BroadcastQueueModernImplTest { assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_TOP_PROCESS, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); doReturn(ActivityManager.PROCESS_STATE_SERVICE).when(mProcess).getSetProcState(); queue.setProcessAndUidState(mProcess, false, false); @@ -580,6 +560,7 @@ public final class BroadcastQueueModernImplTest { List.of(makeMockRegisteredReceiver())), 0); assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); } @Test @@ -594,16 +575,19 @@ public final class BroadcastQueueModernImplTest { assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); doReturn(true).when(mProcess).isPersistent(); queue.setProcessAndUidState(mProcess, false, false); assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_PERSISTENT, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); doReturn(false).when(mProcess).isPersistent(); queue.setProcessAndUidState(mProcess, false, false); assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); } @Test @@ -618,6 +602,7 @@ public final class BroadcastQueueModernImplTest { assertThat(queue.getRunnableAt()).isEqualTo(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_CORE_UID, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); } @Test @@ -636,10 +621,12 @@ public final class BroadcastQueueModernImplTest { assertEquals(Long.MAX_VALUE, queue.getRunnableAt()); assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, queue.getRunnableAtReason()); + assertTrue(queue.shouldBeDeferred()); queue.setProcessAndUidState(mProcess, false, false); assertThat(queue.getRunnableAt()).isEqualTo(timeTickRecord.enqueueTime); assertEquals(BroadcastProcessQueue.REASON_CORE_UID, queue.getRunnableAtReason()); + assertFalse(queue.shouldBeDeferred()); } /** @@ -1575,6 +1562,216 @@ public final class BroadcastQueueModernImplTest { verifyPendingRecords(redQueue, List.of(userPresent, timeTick)); } + @Test + public void testDeliveryDeferredForCached() throws Exception { + final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, + List.of(makeRegisteredReceiver(greenProcess, 0))); + + final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED); + final BroadcastOptions optionsBatteryChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged, + optionsBatteryChanged, + List.of(makeRegisteredReceiver(greenProcess, 10), + makeRegisteredReceiver(redProcess, 0)), + false /* ordered */); + + mImpl.enqueueBroadcastLocked(timeTickRecord); + mImpl.enqueueBroadcastLocked(batteryChangedRecord); + + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + true /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + // Once the broadcasts to green process are deferred, broadcasts to red process + // shouldn't be blocked anymore. + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to green process should be deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED); + final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged, + List.of(makeRegisteredReceiver(greenProcess, 0))); + mImpl.enqueueBroadcastLocked(packageChangedRecord); + + assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to the green process, including the newly enqueued one, should be + // deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + false /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + } + + @Test + public void testDeliveryDeferredForCached_withInfiniteDeferred() throws Exception { + final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastOptions optionsTimeTick = BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, optionsTimeTick, + List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */); + + final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED); + final BroadcastOptions optionsBatteryChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged, + optionsBatteryChanged, + List.of(makeRegisteredReceiver(greenProcess, 10), + makeRegisteredReceiver(redProcess, 0)), + false /* ordered */); + + mImpl.enqueueBroadcastLocked(timeTickRecord); + mImpl.enqueueBroadcastLocked(batteryChangedRecord); + + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + true /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, + greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + // Once the broadcasts to green process are deferred, broadcasts to red process + // shouldn't be blocked anymore. + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to green process should be deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED); + final BroadcastOptions optionsPackageChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged, + optionsPackageChanged, + List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */); + mImpl.enqueueBroadcastLocked(packageChangedRecord); + + assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, + greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to the green process, including the newly enqueued one, should be + // deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + false /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + } + + // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord() + private ProcessRecord makeProcessRecord(ApplicationInfo info) { + final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid)); + r.setPid(mNextPid.incrementAndGet()); + return r; + } + + BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) { + final IIntentReceiver receiver = mock(IIntentReceiver.class); + final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid, + UserHandle.getUserId(app.info.uid), receiver); + return makeRegisteredReceiver(receiverList, priority); + } + private Intent createPackageChangedIntent(int uid, List<String> componentNameList) { final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); packageChangedIntent.putExtra(Intent.EXTRA_UID, uid); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 1c8e9490435d..33645455ca98 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -62,58 +62,36 @@ import android.app.BroadcastOptions; import android.app.IApplicationThread; import android.app.UidObserver; import android.app.usage.UsageEvents.Event; -import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; -import android.content.Context; import android.content.IIntentReceiver; import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManagerInternal; -import android.content.pm.ResolveInfo; import android.os.Binder; import android.os.Bundle; import android.os.DeadObjectException; -import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; import android.os.PowerExemptionManager; import android.os.SystemClock; -import android.os.TestLooperManager; import android.os.UserHandle; -import android.provider.Settings; import android.util.Log; import android.util.Pair; -import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.server.AlarmManagerInternal; -import com.android.server.DropBoxManagerInternal; -import com.android.server.LocalServices; -import com.android.server.am.ActivityManagerService.Injector; -import com.android.server.appop.AppOpsService; -import com.android.server.wm.ActivityTaskManagerService; - import org.junit.After; import org.junit.Assume; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.mockito.ArgumentMatcher; import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.mockito.verification.VerificationMode; -import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.Writer; @@ -126,7 +104,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; @@ -136,13 +113,9 @@ import java.util.function.UnaryOperator; @MediumTest @RunWith(Parameterized.class) @SuppressWarnings("GuardedBy") -public class BroadcastQueueTest { +public class BroadcastQueueTest extends BaseBroadcastQueueTest { private static final String TAG = "BroadcastQueueTest"; - @Rule - public final ApplicationExitInfoTest.ServiceThreadRule - mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule(); - private final Impl mImpl; private enum Impl { @@ -150,30 +123,8 @@ public class BroadcastQueueTest { MODERN, } - private Context mContext; - private HandlerThread mHandlerThread; - private TestLooperManager mLooper; - private AtomicInteger mNextPid; - - @Mock - private AppOpsService mAppOpsService; - @Mock - private ProcessList mProcessList; - @Mock - private DropBoxManagerInternal mDropBoxManagerInt; - @Mock - private PackageManagerInternal mPackageManagerInt; - @Mock - private UsageStatsManagerInternal mUsageStatsManagerInt; - @Mock - private AlarmManagerInternal mAlarmManagerInt; - - private ActivityManagerService mAms; private BroadcastQueue mQueue; - BroadcastConstants mConstants; - private BroadcastSkipPolicy mSkipPolicy; private UidObserver mUidObserver; - private UidObserver mUidCachedStateObserver; /** * Desired behavior of the next @@ -183,11 +134,6 @@ public class BroadcastQueueTest { ProcessStartBehavior.SUCCESS); /** - * Map from PID to registered registered runtime receivers. - */ - private SparseArray<ReceiverList> mRegisteredReceivers = new SparseArray<>(); - - /** * Collection of all active processes during current test run. */ private List<ProcessRecord> mActiveProcesses = new ArrayList<>(); @@ -208,41 +154,8 @@ public class BroadcastQueueTest { @Before public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + super.setUp(); - mHandlerThread = new HandlerThread(TAG); - mHandlerThread.start(); - - // Pause all event processing until a test chooses to resume - mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation() - .acquireLooperManager(mHandlerThread.getLooper())); - - mNextPid = new AtomicInteger(100); - - LocalServices.removeServiceForTest(DropBoxManagerInternal.class); - LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt); - LocalServices.removeServiceForTest(PackageManagerInternal.class); - LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); - LocalServices.removeServiceForTest(AlarmManagerInternal.class); - LocalServices.addService(AlarmManagerInternal.class, mAlarmManagerInt); - doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); - doNothing().when(mPackageManagerInt).notifyComponentUsed(any(), anyInt(), any(), any()); - doAnswer((invocation) -> { - return getUidForPackage(invocation.getArgument(0)); - }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM)); - - final ActivityManagerService realAms = new ActivityManagerService( - new TestInjector(mContext), mServiceThreadRule.getThread()); - realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); - realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); - realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal()); - realAms.mOomAdjuster = spy(realAms.mOomAdjuster); - realAms.mPackageManagerInt = mPackageManagerInt; - realAms.mUsageStatsService = mUsageStatsManagerInt; - realAms.mProcessesReady = true; - mAms = spy(realAms); doAnswer((invocation) -> { Log.v(TAG, "Intercepting startProcessLocked() for " + Arrays.toString(invocation.getArguments())); @@ -321,21 +234,11 @@ public class BroadcastQueueTest { return null; }).when(mAms).registerUidObserver(any(), anyInt(), eq(ActivityManager.PROCESS_STATE_TOP), any()); - doAnswer((invocation) -> { - mUidCachedStateObserver = invocation.getArgument(0); - return null; - }).when(mAms).registerUidObserver(any(), anyInt(), - eq(ActivityManager.PROCESS_STATE_LAST_ACTIVITY), any()); - mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS); mConstants.TIMEOUT = 200; mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500; - mSkipPolicy = spy(new BroadcastSkipPolicy(mAms)); - doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any()); - doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any()); - final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) { public void addBroadcastToHistoryLocked(BroadcastRecord original) { // Ignored @@ -358,7 +261,7 @@ public class BroadcastQueueTest { @After public void tearDown() throws Exception { - mHandlerThread.quit(); + super.tearDown(); // Verify that all processes have finished handling broadcasts for (ProcessRecord app : mActiveProcesses) { @@ -369,26 +272,9 @@ public class BroadcastQueueTest { } } - private class TestInjector extends Injector { - TestInjector(Context context) { - super(context); - } - - @Override - public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile, - Handler handler) { - return mAppOpsService; - } - - @Override - public Handler getUiHandler(ActivityManagerService service) { - return mHandlerThread.getThreadHandler(); - } - - @Override - public ProcessList getProcessList(ActivityManagerService service) { - return mProcessList; - } + @Override + public String getTag() { + return TAG; } private enum ProcessStartBehavior { @@ -534,62 +420,6 @@ public class BroadcastQueueTest { return Pair.create(app.getPid(), intent.getAction()); } - static ApplicationInfo makeApplicationInfo(String packageName) { - return makeApplicationInfo(packageName, packageName, UserHandle.USER_SYSTEM); - } - - static ApplicationInfo makeApplicationInfo(String packageName, String processName, int userId) { - final ApplicationInfo ai = new ApplicationInfo(); - ai.packageName = packageName; - ai.processName = processName; - ai.uid = getUidForPackage(packageName, userId); - return ai; - } - - static ResolveInfo withPriority(ResolveInfo info, int priority) { - info.priority = priority; - return info; - } - - static BroadcastFilter withPriority(BroadcastFilter filter, int priority) { - filter.setPriority(priority); - return filter; - } - - static ResolveInfo makeManifestReceiver(String packageName, String name) { - return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM); - } - - static ResolveInfo makeManifestReceiver(String packageName, String name, int userId) { - return makeManifestReceiver(packageName, packageName, name, userId); - } - - static ResolveInfo makeManifestReceiver(String packageName, String processName, String name, - int userId) { - final ResolveInfo ri = new ResolveInfo(); - ri.activityInfo = new ActivityInfo(); - ri.activityInfo.packageName = packageName; - ri.activityInfo.processName = processName; - ri.activityInfo.name = name; - ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId); - return ri; - } - - private BroadcastFilter makeRegisteredReceiver(ProcessRecord app) { - return makeRegisteredReceiver(app, 0); - } - - private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) { - final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid()); - final IntentFilter filter = new IntentFilter(); - filter.setPriority(priority); - final BroadcastFilter res = new BroadcastFilter(filter, receiverList, - receiverList.app.info.packageName, null, null, null, receiverList.uid, - receiverList.userId, false, false, true); - receiverList.add(res); - return res; - } - private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp, List<Object> receivers) { return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), @@ -776,41 +606,6 @@ public class BroadcastQueueTest { eq(userId), anyInt(), anyInt(), any()); } - static final int USER_GUEST = 11; - - static final String PACKAGE_ANDROID = "android"; - static final String PACKAGE_PHONE = "com.android.phone"; - static final String PACKAGE_RED = "com.example.red"; - static final String PACKAGE_GREEN = "com.example.green"; - static final String PACKAGE_BLUE = "com.example.blue"; - static final String PACKAGE_YELLOW = "com.example.yellow"; - static final String PACKAGE_ORANGE = "com.example.orange"; - - static final String PROCESS_SYSTEM = "system"; - - static final String CLASS_RED = "com.example.red.Red"; - static final String CLASS_GREEN = "com.example.green.Green"; - static final String CLASS_BLUE = "com.example.blue.Blue"; - static final String CLASS_YELLOW = "com.example.yellow.Yellow"; - static final String CLASS_ORANGE = "com.example.orange.Orange"; - - static int getUidForPackage(@NonNull String packageName) { - switch (packageName) { - case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID; - case PACKAGE_PHONE: return android.os.Process.PHONE_UID; - case PACKAGE_RED: return android.os.Process.FIRST_APPLICATION_UID + 1; - case PACKAGE_GREEN: return android.os.Process.FIRST_APPLICATION_UID + 2; - case PACKAGE_BLUE: return android.os.Process.FIRST_APPLICATION_UID + 3; - case PACKAGE_YELLOW: return android.os.Process.FIRST_APPLICATION_UID + 4; - case PACKAGE_ORANGE: return android.os.Process.FIRST_APPLICATION_UID + 5; - default: throw new IllegalArgumentException(); - } - } - - static int getUidForPackage(@NonNull String packageName, int userId) { - return UserHandle.getUid(userId, getUidForPackage(packageName)); - } - /** * Baseline verification of common debugging infrastructure, mostly to make * sure it doesn't crash. diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 6792cfe6e788..3803244c7012 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -43,6 +43,7 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_MAX; import static android.app.NotificationManager.IMPORTANCE_NONE; +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; @@ -72,6 +73,7 @@ import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE; +import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; @@ -163,6 +165,7 @@ import android.app.StatsManager; import android.app.admin.DevicePolicyManagerInternal; import android.app.usage.UsageStatsManagerInternal; import android.companion.AssociationInfo; +import android.companion.AssociationRequest; import android.companion.ICompanionDeviceManager; import android.compat.testing.PlatformCompatChangeRule; import android.content.BroadcastReceiver; @@ -3820,6 +3823,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mListener = mock(ManagedServices.ManagedServiceInfo.class); + mListener.component = new ComponentName(PKG, PKG); when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false); when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener); @@ -3870,6 +3874,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(emptyList()); mListener = mock(ManagedServices.ManagedServiceInfo.class); + mListener.component = new ComponentName(PKG, PKG); when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false); when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener); try { @@ -12777,6 +12782,145 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mSnoozeHelper).clearData(anyInt()); } + @Test + @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setNotificationPolicy_mappedToImplicitRule() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mService.setCallerIsNormalPackage(); + ZenModeHelper zenHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + + NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); + mBinderService.setNotificationPolicy("package", policy); + + verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy)); + } + + @Test + @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setNotificationPolicy_systemCaller_setsGlobalPolicy() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenModeHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + mService.isSystemUid = true; + + NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); + mBinderService.setNotificationPolicy("package", policy); + + verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean()); + } + + @Test + @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setNotificationPolicy_watchCompanionApp_setsGlobalPolicy() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mService.setCallerIsNormalPackage(); + ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenModeHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + when(mCompanionMgr.getAssociations(anyString(), anyInt())) + .thenReturn(ImmutableList.of( + new AssociationInfo.Builder(1, mUserId, "package") + .setDisplayName("My watch") + .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH) + .build())); + + NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); + mBinderService.setNotificationPolicy("package", policy); + + verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean()); + } + + @Test + @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setNotificationPolicy_withoutCompat_setsGlobalPolicy() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mService.setCallerIsNormalPackage(); + ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenModeHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + + NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); + mBinderService.setNotificationPolicy("package", policy); + + verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean()); + } + + @Test + @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void getNotificationPolicy_mappedFromImplicitRule() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mService.setCallerIsNormalPackage(); + ZenModeHelper zenHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + + mBinderService.getNotificationPolicy("package"); + + verify(zenHelper).getNotificationPolicyFromImplicitZenRule(eq("package")); + } + + @Test + @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setInterruptionFilter_mappedToImplicitRule() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mService.setCallerIsNormalPackage(); + ZenModeHelper zenHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + + mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY); + + verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(), + eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); + } + + @Test + @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setInterruptionFilter_systemCaller_setsGlobalPolicy() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mService.setCallerIsNormalPackage(); + ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenModeHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + mService.isSystemUid = true; + + mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY); + + verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), + eq("package"), anyString(), anyInt(), anyBoolean()); + } + + @Test + @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setInterruptionFilter_watchCompanionApp_setsGlobalPolicy() throws RemoteException { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); + mService.mZenModeHelper = zenModeHelper; + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + when(mCompanionMgr.getAssociations(anyString(), anyInt())) + .thenReturn(ImmutableList.of( + new AssociationInfo.Builder(1, mUserId, "package") + .setDisplayName("My watch") + .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH) + .build())); + + mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY); + + verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), + eq("package"), anyString(), anyInt(), anyBoolean()); + } + private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName) throws RemoteException { StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, testName, mUid, 0, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java index 47f15b8df076..1e3b7282e2f7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java @@ -19,6 +19,7 @@ import static com.android.server.notification.SnoozeHelper.CONCURRENT_SNOOZE_LIM import static com.android.server.notification.SnoozeHelper.EXTRA_KEY; import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -73,6 +74,14 @@ import java.io.IOException; public class SnoozeHelperTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "test_channel_id"; + private static final String XML_TAG_NAME = "snoozed-notifications"; + private static final String XML_SNOOZED_NOTIFICATION = "notification"; + private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context"; + private static final String XML_SNOOZED_NOTIFICATION_KEY = "key"; + private static final String XML_SNOOZED_NOTIFICATION_TIME = "time"; + private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id"; + private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version"; + @Mock SnoozeHelper.Callback mCallback; @Mock AlarmManager mAm; @Mock ManagedServices.UserProfiles mUserProfiles; @@ -316,6 +325,53 @@ public class SnoozeHelperTest extends UiServiceTestCase { } @Test + public void testSnoozeLimit_maximumPersisted() throws XmlPullParserException, IOException { + final long snoozeTimeout = 1234; + final String snoozeContext = "ctx"; + // Serialize & deserialize notifications so that only persisted lists are used + TypedXmlSerializer serializer = Xml.newFastSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + serializer.startTag(null, XML_TAG_NAME); + // Serialize maximum number of timed + context snoozed notifications, half of each + for (int i = 0; i < CONCURRENT_SNOOZE_LIMIT; i++) { + final boolean timedNotification = i % 2 == 0; + if (timedNotification) { + serializer.startTag(null, XML_SNOOZED_NOTIFICATION); + } else { + serializer.startTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT); + } + serializer.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, 1); + serializer.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, "key" + i); + if (timedNotification) { + serializer.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME, snoozeTimeout); + serializer.endTag(null, XML_SNOOZED_NOTIFICATION); + } else { + serializer.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID, snoozeContext); + serializer.endTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT); + } + } + serializer.endTag(null, XML_TAG_NAME); + serializer.endDocument(); + serializer.flush(); + + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), "utf-8"); + mSnoozeHelper.readXml(parser, 1); + // Verify that we can't snooze any more notifications + // and that the limit is caused by persisted notifications + assertThat(mSnoozeHelper.canSnooze(1)).isFalse(); + assertThat(mSnoozeHelper.isSnoozed(UserHandle.USER_SYSTEM, "pkg", "key0")).isFalse(); + assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, + "pkg", "key0")).isEqualTo(snoozeTimeout); + assertThat( + mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg", + "key1")).isEqualTo(snoozeContext); + } + + @Test public void testCancelByApp() throws Exception { NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM); NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM); @@ -611,6 +667,7 @@ public class SnoozeHelperTest extends UiServiceTestCase { @Test public void repostGroupSummary_repostsSummary() throws Exception { + final int snoozeDuration = 1000; IntArray profileIds = new IntArray(); profileIds.add(UserHandle.USER_SYSTEM); when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds); @@ -618,10 +675,44 @@ public class SnoozeHelperTest extends UiServiceTestCase { "pkg", 1, "one", UserHandle.SYSTEM, "group1", true); NotificationRecord r2 = getNotificationRecord( "pkg", 2, "two", UserHandle.SYSTEM, "group1", false); - mSnoozeHelper.snooze(r, 1000); - mSnoozeHelper.snooze(r2, 1000); + final long snoozeTime = System.currentTimeMillis() + snoozeDuration; + mSnoozeHelper.snooze(r, snoozeDuration); + mSnoozeHelper.snooze(r2, snoozeDuration); + assertEquals(2, mSnoozeHelper.getSnoozed().size()); + assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was added to the persisted list + assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg", + r.getKey())).isAtLeast(snoozeTime); + + mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey()); + + verify(mCallback, times(1)).repost(UserHandle.USER_SYSTEM, r, false); + verify(mCallback, never()).repost(UserHandle.USER_SYSTEM, r2, false); + + assertEquals(1, mSnoozeHelper.getSnoozed().size()); + assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was removed from the persisted list + assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg", + r.getKey())).isEqualTo(0); + } + + @Test + public void snoozeWithContext_repostGroupSummary_removesPersisted() throws Exception { + final String snoozeContext = "zzzzz"; + IntArray profileIds = new IntArray(); + profileIds.add(UserHandle.USER_SYSTEM); + when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds); + NotificationRecord r = getNotificationRecord( + "pkg", 1, "one", UserHandle.SYSTEM, "group1", true); + NotificationRecord r2 = getNotificationRecord( + "pkg", 2, "two", UserHandle.SYSTEM, "group1", false); + mSnoozeHelper.snooze(r, snoozeContext); + mSnoozeHelper.snooze(r2, snoozeContext); assertEquals(2, mSnoozeHelper.getSnoozed().size()); assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was added to the persisted list + assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, + "pkg", r.getKey())).isEqualTo(snoozeContext); mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey()); @@ -630,6 +721,9 @@ public class SnoozeHelperTest extends UiServiceTestCase { assertEquals(1, mSnoozeHelper.getSnoozed().size()); assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was removed from the persisted list + assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, + "pkg", r.getKey())).isNull(); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java index 27e8f3664a65..8f30f413d4d0 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java @@ -54,6 +54,15 @@ public class TestableNotificationManagerService extends NotificationManagerServi return mRankingHelper; } + /** + * Sets {@link #isSystemUid} and {@link #isSystemAppId} to {@code false}, so that calls to NMS + * methods don't succeed {@link #isCallingUidSystem()} and similar checks. + */ + void setCallerIsNormalPackage() { + isSystemUid = false; + isSystemAppId = false; + } + @Override protected boolean isCallingUidSystem() { countSystemChecks++; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java new file mode 100644 index 000000000000..6cc1c4365fca --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static com.android.server.notification.ZenAdapters.notificationPolicyToZenPolicy; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.NotificationManager.Policy; +import android.service.notification.ZenPolicy; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.UiServiceTestCase; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ZenAdaptersTest extends UiServiceTestCase { + + @Test + public void notificationPolicyToZenPolicy_allCallers() { + Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_ANY, 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_ANYONE); + } + + @Test + public void notificationPolicyToZenPolicy_starredCallers() { + Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_STARRED, + 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED); + } + + @Test + public void notificationPolicyToZenPolicy_repeatCallers() { + Policy policy = new Policy(Policy.PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE); + } + + @Test + public void notificationPolicyToZenPolicy_noCallers() { + Policy policy = new Policy(0, 0, 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE); + } + + @Test + public void notificationPolicyToZenPolicy_conversationsAllowedSendersUnset() { + Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CONVERSATIONS, 0, 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo(ZenPolicy.STATE_UNSET); + } + + @Test + public void notificationPolicyToZenPolicy_conversationsNotAllowedSendersUnset() { + Policy policy = new Policy(0, 0, 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo( + ZenPolicy.STATE_DISALLOW); + } + + @Test + public void notificationPolicyToZenPolicy_setEffects() { + Policy policy = new Policy(0, 0, 0, + Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_DISALLOW); + + assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_ALLOW); + } + + @Test + public void notificationPolicyToZenPolicy_unsetEffects() { + Policy policy = new Policy(0, 0, 0); + + ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); + + assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_UNSET); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index e8201fdef8de..37aeb57f728e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -22,6 +22,7 @@ import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ENABLED; import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE; +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS; @@ -32,6 +33,7 @@ import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REMINDERS import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM; import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; @@ -40,8 +42,11 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; +import static android.provider.Settings.Global.ZEN_MODE_OFF; import static android.service.notification.Condition.STATE_FALSE; import static android.service.notification.Condition.STATE_TRUE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS; import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED; @@ -50,6 +55,8 @@ import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW; import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -72,6 +79,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -82,6 +90,7 @@ import android.app.NotificationManager.Policy; import android.content.ComponentName; import android.content.ContentResolver; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -92,6 +101,7 @@ import android.media.AudioManagerInternal; import android.media.AudioSystem; import android.media.VolumePolicy; import android.net.Uri; +import android.os.Parcel; import android.os.Process; import android.os.UserHandle; import android.platform.test.flag.junit.SetFlagsRule; @@ -100,6 +110,7 @@ import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ScheduleInfo; +import android.service.notification.ZenModeConfig.ZenRule; import android.service.notification.ZenModeDiff; import android.service.notification.ZenPolicy; import android.test.suitebuilder.annotation.SmallTest; @@ -124,6 +135,7 @@ import com.android.server.UiServiceTestCase; import com.android.server.notification.ManagedServices.UserProfiles; import com.google.common.collect.ImmutableList; +import com.google.common.truth.Correspondence; import com.google.protobuf.InvalidProtocolBufferException; import org.junit.Before; @@ -157,27 +169,29 @@ public class ZenModeHelperTest extends UiServiceTestCase { private static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE"; private static final String SCHEDULE_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE"; private static final String CUSTOM_PKG_NAME = "not.android"; + private static final String CUSTOM_APP_LABEL = "This is not Android"; private static final int CUSTOM_PKG_UID = 1; private static final String CUSTOM_RULE_ID = "custom_rule"; - private final String NAME = "name"; - private final ComponentName OWNER = new ComponentName("pkg", "cls"); - private final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act"); - private final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build(); - private final Uri CONDITION_ID = new Uri.Builder().scheme("scheme") + private static final String NAME = "name"; + private static final ComponentName OWNER = new ComponentName("pkg", "cls"); + private static final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act"); + private static final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build(); + private static final Uri CONDITION_ID = new Uri.Builder().scheme("scheme") .authority("authority") .appendPath("path") .appendPath("test") .build(); - private final Condition CONDITION = new Condition(CONDITION_ID, "", Condition.STATE_TRUE); - private final String TRIGGER_DESC = "Every Night, 10pm to 6am"; - private final int TYPE = TYPE_BEDTIME; - private final boolean ALLOW_MANUAL = true; - private final int ICON_RES_ID = 1234; - private final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS; - private final boolean ENABLED = true; - private final int CREATION_TIME = 123; + private static final Condition CONDITION = new Condition(CONDITION_ID, "", + Condition.STATE_TRUE); + private static final String TRIGGER_DESC = "Every Night, 10pm to 6am"; + private static final int TYPE = TYPE_BEDTIME; + private static final boolean ALLOW_MANUAL = true; + private static final int ICON_RES_ID = 1234; + private static final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS; + private static final boolean ENABLED = true; + private static final int CREATION_TIME = 123; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -227,6 +241,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .thenReturn(CUSTOM_PKG_UID); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( new String[] {pkg}); + ApplicationInfo mockAppInfo = mock(ApplicationInfo.class); + when(mockAppInfo.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL); + when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt())) + .thenReturn(mockAppInfo); mZenModeHelper.mPm = mPackageManager; mZenModeEventLogger.reset(); @@ -334,7 +352,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOff_NoMuteApplied() { - mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0); @@ -635,7 +653,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 3. apply zen off - verify zen is set to previous ringer (normal) when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.applyZenToRingerMode(); verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, mZenModeHelper.TAG); @@ -721,7 +739,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 3. apply zen off - verify ringer remains normal when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.applyZenToRingerMode(); verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, mZenModeHelper.TAG); @@ -746,7 +764,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 3. apply zen-off - verify ringer is still silent when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.applyZenToRingerMode(); verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT, mZenModeHelper.TAG); @@ -781,7 +799,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 4. apply zen off - verify ringer still silenced when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.applyZenToRingerMode(); verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT, mZenModeHelper.TAG); @@ -795,7 +813,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // apply zen off multiple times - verify ringer is not set to normal when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.mConfig = null; // will evaluate config to zen mode off for (int i = 0; i < 3; i++) { // if zen doesn't change, zen should not reapply itself to the ringer @@ -809,7 +827,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testSilentRingerSavedOnZenOff_startsZenOn() { AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class); mZenModeHelper.mAudioManager = mAudioManager; - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.mConfig = new ZenModeConfig(); // previously set silent ringer @@ -836,7 +854,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testVibrateRingerSavedOnZenOff_startsZenOn() { AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class); mZenModeHelper.mAudioManager = mAudioManager; - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.mConfig = new ZenModeConfig(); // previously set silent ringer @@ -1209,7 +1227,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowMedia(false) .allowRepeatCallers(false) .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) - .allowMessages(ZenPolicy.PEOPLE_TYPE_CONTACTS) + .allowMessages(PEOPLE_TYPE_CONTACTS) .allowEvents(true) .allowReminders(false) .build(); @@ -2023,10 +2041,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode); // and also that it works to turn it back off again - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "", Process.SYSTEM_UID, true); - assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode); + assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode); } @Test @@ -2041,7 +2059,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Now turn zen mode off, but via a different package UID -- this should get registered as // "not an action by the user" because some other app is changing zen mode - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID, + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID, false); // In total, this should be 2 loggable changes @@ -2060,7 +2078,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // - resulting DNDPolicyProto the same as the values in setupZenConfig() assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); - assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); + assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); @@ -2080,7 +2098,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), mZenModeEventLogger.getEventId(1)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1)); - assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1)); + assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1)); assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(1)); assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); @@ -2144,7 +2162,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // - zen policy is the same as the set-up zen config assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); - assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); + assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); @@ -2157,7 +2175,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), mZenModeEventLogger.getEventId(1)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1)); - assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1)); + assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1)); assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1)); assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); assertTrue(mZenModeEventLogger.getIsUserAction(1)); @@ -2201,7 +2219,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode // is off. - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "", Process.SYSTEM_UID, true); // Change the policy again @@ -2305,7 +2323,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // what the event should reflect. At this time, the policy is the same as initial setup. assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); - assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); + assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertFalse(mZenModeEventLogger.getIsUserAction(0)); @@ -2355,7 +2373,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.evaluateZenModeLocked("test", true); // Check that the change actually took: zen mode should be off now - assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode); + assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode); // but still, nothing should've been logged assertEquals(0, mZenModeEventLogger.numLoggedChanges()); @@ -2483,7 +2501,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { true); // Turn off manual mode, call from a package: don't reset UID even though enabler is set - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, CUSTOM_PKG_NAME, "", 12345, false); // And likewise when turning it back on again @@ -2660,8 +2678,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testCreateAutomaticZenRule_allFields() { + public void zenRuleToAutomaticZenRule_allFields() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( + new String[] {OWNER.getPackageName()}); + ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.configurationActivity = CONFIG_ACTIVITY; rule.component = OWNER; @@ -2682,7 +2703,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.iconResId = ICON_RES_ID; rule.triggerDescription = TRIGGER_DESC; - AutomaticZenRule actual = mZenModeHelper.createAutomaticZenRule(rule); + mZenModeHelper.mConfig.automaticRules.put(rule.id, rule); + AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(rule.id); assertEquals(NAME, actual.getName()); assertEquals(OWNER, actual.getOwner()); @@ -2915,8 +2937,253 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing); } + @Test + public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_TIMESTAMPS) + .containsExactly( + expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + null, true)); + } + + @Test + public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_ALARMS); + + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_TIMESTAMPS) + .containsExactly( + expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true)); + } + + @Test + public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state) + .isEqualTo(STATE_TRUE); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_OFF); + + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state) + .isEqualTo(STATE_FALSE); + } + + @Test + public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_OFF); + + assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); + } + + @Test + public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse(); + + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue(); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_ALARMS); + + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse(); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state) + .isEqualTo(STATE_TRUE); + } + + @Test + public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() { + mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + withoutWtfCrash( + () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS)); + + assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); + } + + @Test + public void applyGlobalPolicyAsImplicitZenRule_createsImplicitRule() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, + PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, + Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy); + + ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() + .disallowAllSounds() + .allowCalls(PEOPLE_TYPE_CONTACTS) + .allowConversations(CONVERSATION_SENDERS_IMPORTANT) + .hideAllVisualEffects() + .build(); + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_TIMESTAMPS) + .containsExactly( + expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + expectedZenPolicy, /* conditionActive= */ null)); + } + + @Test + public void applyGlobalPolicyAsImplicitZenRule_updatesImplicitRule() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + Policy original = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, + PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, + Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + original); + + // Change priorityCallSenders: contacts -> starred. + Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, + PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, + Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated); + + ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() + .disallowAllSounds() + .allowCalls(PEOPLE_TYPE_STARRED) + .allowConversations(CONVERSATION_SENDERS_IMPORTANT) + .hideAllVisualEffects() + .build(); + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_TIMESTAMPS) + .containsExactly( + expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + expectedZenPolicy, /* conditionActive= */ null)); + } + + @Test + public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() { + mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + withoutWtfCrash( + () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, new Policy(0, 0, 0))); + + assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); + } + + @Test + public void getNotificationPolicyFromImplicitZenRule_returnsSetPolicy() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + Policy writtenPolicy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, + PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, + Policy.getAllSuppressedVisualEffects(), STATE_FALSE, + CONVERSATION_SENDERS_IMPORTANT); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + writtenPolicy); + + Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( + CUSTOM_PKG_NAME); + + assertThat(readPolicy).isEqualTo(writtenPolicy); + } + + @Test + public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, + ZEN_MODE_ALARMS); + mZenModeHelper.mConfig.allowCalls = true; + mZenModeHelper.mConfig.allowConversations = false; + + Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( + CUSTOM_PKG_NAME); + + assertThat(readPolicy).isNotNull(); + assertThat(readPolicy.allowCalls()).isTrue(); + assertThat(readPolicy.allowConversations()).isFalse(); + } + + @Test + public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + mZenModeHelper.mConfig.allowCalls = true; + mZenModeHelper.mConfig.allowConversations = false; + + Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( + CUSTOM_PKG_NAME); + + assertThat(readPolicy).isNotNull(); + assertThat(readPolicy.allowCalls()).isTrue(); + assertThat(readPolicy.allowConversations()).isFalse(); + } + + private static final Correspondence<ZenRule, ZenRule> IGNORE_TIMESTAMPS = + Correspondence.transforming(zr -> { + Parcel p = Parcel.obtain(); + try { + zr.writeToParcel(p, 0); + p.setDataPosition(0); + ZenRule copy = new ZenRule(p); + copy.creationTime = 0; + return copy; + } finally { + p.recycle(); + } + }, + "Ignoring timestamps"); + + private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, + @Nullable Boolean conditionActive) { + ZenRule rule = new ZenModeConfig.ZenRule(); + rule.id = "implicit_" + ownerPkg; + rule.conditionId = Uri.parse("condition://android/implicit/" + ownerPkg); + if (conditionActive != null) { + rule.condition = conditionActive + ? new Condition(rule.conditionId, + mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE) + : new Condition(rule.conditionId, + mContext.getString(R.string.zen_mode_implicit_deactivated), + STATE_FALSE); + } + rule.zenMode = zenMode; + rule.zenPolicy = policy; + rule.pkg = ownerPkg; + rule.name = CUSTOM_APP_LABEL; + rule.enabled = true; + return rule; + } + private void setupZenConfig() { - mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF; + mZenModeHelper.mZenMode = ZEN_MODE_OFF; mZenModeHelper.mConfig.allowAlarms = false; mZenModeHelper.mConfig.allowMedia = false; mZenModeHelper.mConfig.allowSystem = false; @@ -2965,6 +3232,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber()); } + private static void withoutWtfCrash(Runnable test) { + Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {}); + try { + test.run(); + } finally { + Log.setWtfHandler(oldHandler); + } + } + /** * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml() */ |