diff options
38 files changed, 1232 insertions, 672 deletions
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 2b096eaab237..3e6f18e8e24b 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -188,9 +188,17 @@ public class Cuj { */ public static final int CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU = 112; + /** Track Launcher Keyboard Quick Switch View opening animation */ + public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN = 113; + + /** Track Launcher Keyboard Quick Switch View closing animation */ + public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE = 114; + + /** Track launching an app through the Launcher Keyboard Quick Switch View */ + public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH = 115; // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. - @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU; + @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; /** @hide */ @IntDef({ @@ -294,7 +302,10 @@ public class Cuj { CUJ_DESKTOP_MODE_EXIT_MODE, CUJ_DESKTOP_MODE_MINIMIZE_WINDOW, CUJ_DESKTOP_MODE_DRAG_WINDOW, - CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP + CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN, + CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE, + CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH }) @Retention(RetentionPolicy.SOURCE) public @interface CujType {} @@ -409,6 +420,9 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MINIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MINIMIZE_WINDOW; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_DRAG_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_DRAG_WINDOW; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; } private Cuj() { @@ -629,6 +643,12 @@ public class Cuj { return "DESKTOP_MODE_DRAG_WINDOW"; case CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP: return "STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP"; + case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN: + return "LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN"; + case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE: + return "LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE"; + case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH: + return "LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 32c4830c674d..652cba7ed00d 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -211,7 +211,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { */ public int startLoggingToLogcat(String[] groups, ILogger logger) { if (mViewerConfigReader != null) { - mViewerConfigReader.loadViewerConfig(logger); + mViewerConfigReader.loadViewerConfig(groups, logger); } return setTextLogging(true, logger, groups); } @@ -224,7 +224,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { */ public int stopLoggingToLogcat(String[] groups, ILogger logger) { if (mViewerConfigReader != null) { - mViewerConfigReader.unloadViewerConfig(); + mViewerConfigReader.unloadViewerConfig(groups, logger); } return setTextLogging(false, logger, groups); } @@ -268,7 +268,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { } case "enable-text" -> { if (mViewerConfigReader != null) { - mViewerConfigReader.loadViewerConfig(logger); + mViewerConfigReader.loadViewerConfig(groups, logger); } return setTextLogging(true, logger, groups); } diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index b7b24241c26a..bb6c8b7a9698 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -1,20 +1,32 @@ package com.android.internal.protolog; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME; + import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID; -import android.util.ArrayMap; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Log; +import android.util.LongSparseArray; import android.util.proto.ProtoInputStream; import com.android.internal.protolog.common.ILogger; import java.io.IOException; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; public class ProtoLogViewerConfigReader { private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; - private Map<Long, String> mLogMessageMap = null; + private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>(); + private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>(); public ProtoLogViewerConfigReader( ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { @@ -26,39 +38,62 @@ public class ProtoLogViewerConfigReader { * or the viewer config is not loaded into memory. */ public synchronized String getViewerString(long messageHash) { - if (mLogMessageMap != null) { - return mLogMessageMap.get(messageHash); - } else { - return null; - } + return mLogMessageMap.get(messageHash); + } + + public synchronized void loadViewerConfig(String[] groups) { + loadViewerConfig(groups, (message) -> {}); } /** * Loads the viewer config into memory. No-op if already loaded in memory. */ - public synchronized void loadViewerConfig(ILogger logger) { - if (mLogMessageMap != null) { - return; - } + public synchronized void loadViewerConfig(String[] groups, @NonNull ILogger logger) { + for (String group : groups) { + if (mGroupHashes.containsKey(group)) { + continue; + } + + try { + Map<Long, String> mappings = loadViewerConfigMappingForGroup(group); + mGroupHashes.put(group, mappings.keySet()); + for (Long key : mappings.keySet()) { + mLogMessageMap.put(key, mappings.get(key)); + } - try { - doLoadViewerConfig(); - logger.log("Loaded " + mLogMessageMap.size() + " log definitions"); - } catch (IOException e) { - logger.log("Unable to load log definitions: " - + "IOException while processing viewer config" + e); + logger.log("Loaded " + mLogMessageMap.size() + " log definitions"); + } catch (IOException e) { + logger.log("Unable to load log definitions: " + + "IOException while processing viewer config" + e); + } } } + public synchronized void unloadViewerConfig(String[] groups) { + unloadViewerConfig(groups, (message) -> {}); + } + /** * Unload the viewer config from memory. */ - public synchronized void unloadViewerConfig() { - mLogMessageMap = null; + public synchronized void unloadViewerConfig(String[] groups, @NonNull ILogger logger) { + for (String group : groups) { + if (!mGroupHashes.containsKey(group)) { + continue; + } + + final Set<Long> hashes = mGroupHashes.get(group); + for (Long hash : hashes) { + logger.log("Unloading viewer config hash " + hash); + mLogMessageMap.remove(hash); + } + } } - private void doLoadViewerConfig() throws IOException { - mLogMessageMap = new ArrayMap<>(); + private Map<Long, String> loadViewerConfigMappingForGroup(String group) throws IOException { + Long targetGroupId = loadGroupId(group); + + final Map<Long, String> hashesForGroup = new TreeMap<>(); final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream(); while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { @@ -67,6 +102,7 @@ public class ProtoLogViewerConfigReader { long messageId = 0; String message = null; + int groupId = 0; while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (pis.getFieldNumber()) { case (int) MESSAGE_ID: @@ -75,9 +111,16 @@ public class ProtoLogViewerConfigReader { case (int) MESSAGE: message = pis.readString(MESSAGE); break; + case (int) GROUP_ID: + groupId = pis.readInt(GROUP_ID); + break; } } + if (groupId == 0) { + throw new IOException("Failed to get group id"); + } + if (messageId == 0) { throw new IOException("Failed to get message id"); } @@ -86,10 +129,45 @@ public class ProtoLogViewerConfigReader { throw new IOException("Failed to get message string"); } - mLogMessageMap.put(messageId, message); + if (groupId == targetGroupId) { + hashesForGroup.put(messageId, message); + } + + pis.end(inMessageToken); + } + } + + return hashesForGroup; + } + + private Long loadGroupId(String group) throws IOException { + final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream(); + + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) GROUPS) { + final long inMessageToken = pis.start(GROUPS); + + long groupId = 0; + String groupName = null; + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) ID: + groupId = pis.readInt(ID); + break; + case (int) NAME: + groupName = pis.readString(NAME); + break; + } + } + + if (Objects.equals(groupName, group)) { + return groupId; + } pis.end(inMessageToken); } } + + throw new RuntimeException("Group " + group + "not found in viewer config"); } } diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index dbcad8aab45b..a00d003001e6 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -224,7 +224,6 @@ android_library { "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:iconloader_base", "com_android_wm_shell_flags_lib", - "com.android.window.flags.window-aconfig-java", "WindowManager-Shell-proto", "WindowManager-Shell-shared", "perfetto_trace_java_protos", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 6315e6906842..580724666949 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1107,6 +1107,10 @@ class DesktopTasksController( if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } + if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) { + // Remove wallpaper activity when leaving desktop mode + removeWallpaperActivity(wct) + } } /** @@ -1122,6 +1126,10 @@ class DesktopTasksController( // The task's density may have been overridden in freeform; revert it here as we don't // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) + if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) { + // Remove wallpaper activity when leaving desktop mode + removeWallpaperActivity(wct) + } } /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index f67043488bec..8558a77e4e7e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -935,6 +935,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -946,6 +964,44 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + // Setup task2 + setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val task1Change = assertNotNull(wct.changes[task1.token.asBinder()]) + assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Does not remove wallpaper activity, as desktop still has a visible desktop task + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test fun moveToFullscreen_nonExistentTask_doesNothing() { controller.moveToFullscreen(999, transitionSource = UNKNOWN) verifyExitDesktopWCTNotExecuted() @@ -1769,6 +1825,49 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId, + visible = false) + + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { val spyController = spy(controller) @@ -1977,6 +2076,7 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(null)) } + @Test fun enterSplit_freeformTaskIsMovedToSplit() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -1986,14 +2086,67 @@ class DesktopTasksControllerTest : ShellTestCase() { task2.isFocused = true task3.isFocused = false - controller.enterSplit(DEFAULT_DISPLAY, false) + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) verify(splitScreenController) .requestEnterSplitSelect( - task2, + eq(task2), any(), - SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, - task2.configuration.windowConfiguration.bounds) + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + } + + @Test + fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId, + visible = false) + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + // Removes wallpaper activity when leaving desktop + wctArgument.value.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wctArgument.value.hierarchyOps).isEmpty() } @Test diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index db71d72752ce..187187326ad5 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -581,7 +581,6 @@ android_library { "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", "androidx.lifecycle_lifecycle-viewmodel-compose", - "device_policy_aconfig_flags_lib", ], libs: [ "keepanno-annotations", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 6e6e4b2facb3..b580eb1dfef6 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1171,3 +1171,12 @@ flag { bug: "345227709" } +flag { + namespace: "systemui" + name: "register_content_observers_async" + description: "Use new Async API to register content observers" + bug: "316922634" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt index d7b7cfe11610..583c10fe429e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -20,7 +20,6 @@ import android.app.Flags import android.os.UserHandle import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS import android.provider.Settings.Global.ZEN_MODE_OFF @@ -42,7 +41,6 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class ModesTileDataInteractorTest : SysuiTestCase() { private val zenModeRepository = FakeZenModeRepository() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index b5e47d167fa3..fd1b21332973 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -85,7 +85,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -182,7 +181,6 @@ class SceneContainerStartableTest : SysuiTestCase() { kosmos.headsUpNotificationRepository.activeHeadsUpRows.value = buildNotificationRows(isPinned = false) - advanceTimeBy(50L) // account for HeadsUpNotificationInteractor debounce assertThat(isVisible).isFalse() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index 5ef3485a8e51..8b4265f552fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -23,7 +23,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -280,64 +279,6 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { } @Test - fun isHeadsUpOrAnimatingAway_falseOnStart() = - testScope.runTest { - val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway) - - runCurrent() - - assertThat(isHeadsUpOrAnimatingAway).isFalse() - } - - @Test - fun isHeadsUpOrAnimatingAway_hasPinnedRows() = - testScope.runTest { - val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway) - - // WHEN a row is pinned - headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) - runCurrent() - - assertThat(isHeadsUpOrAnimatingAway).isTrue() - } - - @Test - fun isHeadsUpOrAnimatingAway_headsUpAnimatingAway() = - testScope.runTest { - val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway) - - // WHEN the last row is animating away - headsUpRepository.setHeadsUpAnimatingAway(true) - runCurrent() - - assertThat(isHeadsUpOrAnimatingAway).isTrue() - } - - @Test - fun isHeadsUpOrAnimatingAway_headsUpAnimatingAwayDebounced() = - testScope.runTest { - val values by collectValues(underTest.isHeadsUpOrAnimatingAway) - - // GIVEN a row is pinned - headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) - runCurrent() - assertThat(values.size).isEqualTo(2) - assertThat(values.first()).isFalse() // initial value - assertThat(values.last()).isTrue() - - // WHEN the last row is removed - headsUpRepository.setNotifications(emptyList()) - runCurrent() - // AND starts to animate away - headsUpRepository.setHeadsUpAnimatingAway(true) - runCurrent() - - // THEN isHeadsUpOrAnimatingAway remained true - assertThat(values.size).isEqualTo(2) - assertThat(values.last()).isTrue() - } - - @Test fun showHeadsUpStatusBar_true() = testScope.runTest { val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt index 9e5379283f49..49817b263583 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.drawable.Icon import android.hardware.input.InputManager import android.util.Log +import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.KeyboardShortcutGroup @@ -47,8 +48,11 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @SysUISingleton @@ -56,6 +60,7 @@ class ShortcutHelperCategoriesRepository @Inject constructor( private val context: Context, + @Background private val backgroundScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource, @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource, @@ -75,81 +80,82 @@ constructor( } } - val systemShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - System, - systemShortcutsSource.shortcutGroups(it.id), - keepIcons = true, + val categories: Flow<List<ShortcutCategory>> = + activeInputDevice + .map { + if (it == null) { + return@map emptyList() + } + return@map listOfNotNull( + fetchSystemShortcuts(it), + fetchMultiTaskingShortcuts(it), + fetchAppCategoriesShortcuts(it), + fetchImeShortcuts(it), + fetchCurrentAppShortcuts(it), ) - } else { - null } - } + .stateIn( + scope = backgroundScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) - val multitaskingShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - MultiTasking, - multitaskingShortcutsSource.shortcutGroups(it.id), - keepIcons = true, - ) - } else { - null - } - } + private suspend fun fetchSystemShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + System, + systemShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = true, + ) - val appCategoriesShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - AppCategories, - appCategoriesShortcutsSource.shortcutGroups(it.id), - keepIcons = true, - ) - } else { - null - } - } + private suspend fun fetchMultiTaskingShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + MultiTasking, + multitaskingShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = true, + ) - val imeShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - InputMethodEditor, - inputShortcutsSource.shortcutGroups(it.id), - keepIcons = false, - ) - } else { - null - } + private suspend fun fetchAppCategoriesShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + AppCategories, + appCategoriesShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = true, + ) + + private suspend fun fetchImeShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + InputMethodEditor, + inputShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = false, + ) + + private suspend fun fetchCurrentAppShortcuts(inputDevice: InputDevice): ShortcutCategory? { + val shortcutGroups = currentAppShortcutsSource.shortcutGroups(inputDevice.id) + val categoryType = getCurrentAppShortcutCategoryType(shortcutGroups) + return if (categoryType == null) { + null + } else { + toShortcutCategory( + inputDevice.keyCharacterMap, + categoryType, + shortcutGroups, + keepIcons = false + ) } + } - val currentAppShortcutsCategory: Flow<ShortcutCategory?> = - activeInputDevice.map { - if (it != null) { - val shortcutGroups = currentAppShortcutsSource.shortcutGroups(it.id) - val categoryType = getCurrentAppShortcutCategoryType(shortcutGroups) - if (categoryType == null) { - null - } else { - toShortcutCategory( - it.keyCharacterMap, - categoryType, - shortcutGroups, - keepIcons = false - ) - } - } else { - null - } + private fun getCurrentAppShortcutCategoryType( + shortcutGroups: List<KeyboardShortcutGroup> + ): ShortcutCategoryType? { + return if (shortcutGroups.isEmpty()) { + null + } else { + CurrentApp(packageName = shortcutGroups[0].packageName.toString()) } + } private fun toShortcutCategory( keyCharacterMap: KeyCharacterMap, @@ -174,16 +180,6 @@ constructor( } } - private fun getCurrentAppShortcutCategoryType( - shortcutGroups: List<KeyboardShortcutGroup> - ): ShortcutCategoryType? { - return if (shortcutGroups.isEmpty()) { - null - } else { - CurrentApp(packageName = shortcutGroups[0].packageName.toString()) - } - } - private fun toShortcuts( keyCharacterMap: KeyCharacterMap, infoList: List<KeyboardShortcutInfo>, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt index f215c74b3255..6f19561dd87b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt @@ -23,7 +23,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map @SysUISingleton class ShortcutHelperCategoriesInteractor @@ -33,14 +33,8 @@ constructor( ) { val shortcutCategories: Flow<List<ShortcutCategory>> = - combine( - categoriesRepository.systemShortcutsCategory, - categoriesRepository.multitaskingShortcutsCategory, - categoriesRepository.imeShortcutsCategory, - categoriesRepository.appCategoriesShortcutsCategory, - categoriesRepository.currentAppShortcutsCategory - ) { shortcutCategories -> - shortcutCategories.filterNotNull().map { groupSubCategoriesInCategory(it) } + categoriesRepository.categories.map { categories -> + categories.map { category -> groupSubCategoriesInCategory(category) } } private fun groupSubCategoriesInCategory(shortcutCategory: ShortcutCategory): ShortcutCategory { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt index e602cad30daa..ad258f435d63 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyboard.shortcut.ui.viewmodel import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -40,8 +39,8 @@ constructor( ) { val shouldShow = - stateInteractor.state - .map { it is ShortcutHelperState.Active } + categoriesInteractor.shortcutCategories + .map { it.isNotEmpty() } .distinctUntilChanged() .flowOn(backgroundDispatcher) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index ab432d6e26c3..c0049d4e2e6c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -31,7 +31,6 @@ import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockId import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.util.kotlin.combine @@ -104,21 +103,21 @@ constructor( val clockShouldBeCentered: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.shadeMode, + shadeInteractor.isShadeLayoutWide, activeNotificationsInteractor.areAnyNotificationsPresent, keyguardInteractor.isActiveDreamLockscreenHosted, isOnAod, headsUpNotificationInteractor.isHeadsUpOrAnimatingAway, keyguardInteractor.isDozing, ) { - shadeMode, + isShadeLayoutWide, areAnyNotificationsPresent, isActiveDreamLockscreenHosted, isOnAod, isHeadsUp, isDozing -> when { - shadeMode != ShadeMode.Split -> true + !isShadeLayoutWide -> true !areAnyNotificationsPresent -> true isActiveDreamLockscreenHosted -> true // Pulsing notification appears on the right. Move clock left to avoid overlap. diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java index c971f547c302..edc49cac2f92 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java @@ -183,9 +183,9 @@ public class InternetDialogDelegate implements Context context, InternetDialogManager internetDialogManager, InternetDialogController internetDialogController, - @Assisted(ABOVE_STATUS_BAR) boolean canConfigMobileData, - @Assisted(CAN_CONFIG_MOBILE_DATA) boolean canConfigWifi, - @Assisted(CAN_CONFIG_WIFI) boolean aboveStatusBar, + @Assisted(CAN_CONFIG_MOBILE_DATA) boolean canConfigMobileData, + @Assisted(CAN_CONFIG_WIFI) boolean canConfigWifi, + @Assisted(ABOVE_STATUS_BAR) boolean aboveStatusBar, @Assisted CoroutineScope coroutineScope, UiEventLogger uiEventLogger, DialogTransitionAnimator dialogTransitionAnimator, 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 c1f8a0be646d..45f359efbb7a 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 @@ -55,7 +55,11 @@ interface ShadeInteractor : BaseShadeInteractor { /** Whether the shade can be expanded from QQS to QS. */ val isExpandToQsEnabled: Flow<Boolean> - /** The version of the shade layout to use. */ + /** + * The version of the shade layout to use. + * + * Note: Most likely, you want to read [isShadeLayoutWide] instead of this. + */ val shadeMode: StateFlow<ShadeMode> /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt index 8214a24a9a38..a8199a402ef1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt @@ -29,6 +29,8 @@ sealed interface ShadeMode { /** * The split shade where, on large screens and unfolded foldables, the QS and notification parts * are placed side-by-side and expand/collapse as a single panel. + * + * Note: This isn't the only mode where the shade is wide. */ data object Split : ShadeMode diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index eebbb13005b9..bf44b9f3cf78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) +@file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.statusbar.notification.domain.interactor @@ -25,17 +25,14 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart class HeadsUpNotificationInteractor @Inject @@ -50,48 +47,54 @@ constructor( val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow /** Set of currently pinned top-level heads up rows to be displayed. */ - val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> = - headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories -> - if (repositories.isNotEmpty()) { - val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> = - repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } } - combine(toCombine) { pairs -> - pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet() + val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(emptySet()) + } else { + headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories -> + if (repositories.isNotEmpty()) { + val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> = + repositories.map { repo -> + repo.isPinned.map { isPinned -> repo to isPinned } + } + combine(toCombine) { pairs -> + pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet() + } + } else { + // if the set is empty, there are no flows to combine + flowOf(emptySet()) } - } else { - // if the set is empty, there are no flows to combine - flowOf(emptySet()) } } + } /** Are there any pinned heads up rows to display? */ - val hasPinnedRows: Flow<Boolean> = - headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> - if (rows.isNotEmpty()) { - combine(rows.map { it.isPinned }) { pins -> pins.any { it } } - } else { - // if the set is empty, there are no flows to combine - flowOf(false) + val hasPinnedRows: Flow<Boolean> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> + if (rows.isNotEmpty()) { + combine(rows.map { it.isPinned }) { pins -> pins.any { it } } + } else { + // if the set is empty, there are no flows to combine + flowOf(false) + } } } + } - val isHeadsUpOrAnimatingAway: Flow<Boolean> = - combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { - hasPinnedRows, - animatingAway -> - hasPinnedRows || animatingAway - } - .debounce { isHeadsUpOrAnimatingAway -> - if (isHeadsUpOrAnimatingAway) { - 0 - } else { - // When the last pinned entry is removed from the [HeadsUpRepository], - // there might be a delay before the View starts animating. - 50L + val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { + hasPinnedRows, + animatingAway -> + hasPinnedRows || animatingAway } - } - .onStart { emit(false) } // emit false, so we don't wait for the initial update - .distinctUntilChanged() + } + } private val canShowHeadsUp: Flow<Boolean> = combine( @@ -109,10 +112,15 @@ constructor( } } - val showHeadsUpStatusBar: Flow<Boolean> = - combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp -> - hasPinnedRows && canShowHeadsUp + val showHeadsUpStatusBar: Flow<Boolean> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp -> + hasPinnedRows && canShowHeadsUp + } } + } fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor = HeadsUpRowInteractor(key as HeadsUpRowRepository) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index a11cbc3bf231..98869bef5bf0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -37,6 +37,7 @@ import androidx.annotation.VisibleForTesting; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.Dumpable; +import com.android.systemui.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -54,6 +55,7 @@ import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; +import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; import java.util.Optional; @@ -86,6 +88,7 @@ public class DozeParameters implements private final FoldAodAnimationController mFoldAodAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; private final UserTracker mUserTracker; + private final SecureSettings mSecureSettings; private boolean mDozeAlwaysOn; private boolean mControlScreenOffAnimation; @@ -130,7 +133,8 @@ public class DozeParameters implements ConfigurationController configurationController, StatusBarStateController statusBarStateController, UserTracker userTracker, - DozeInteractor dozeInteractor) { + DozeInteractor dozeInteractor, + SecureSettings secureSettings) { mResources = resources; mAmbientDisplayConfiguration = ambientDisplayConfiguration; mAlwaysOnPolicy = alwaysOnDisplayPolicy; @@ -144,6 +148,7 @@ public class DozeParameters implements mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; mUserTracker = userTracker; mDozeInteractor = dozeInteractor; + mSecureSettings = secureSettings; keyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); tunerService.addTunable( @@ -160,7 +165,8 @@ public class DozeParameters implements mFoldAodAnimationController.addCallback(this); } - SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler); + SettingsObserver quickPickupSettingsObserver = + new SettingsObserver(context, handler, mSecureSettings); quickPickupSettingsObserver.observe(); batteryController.addCallback(new BatteryStateChangeCallback() { @@ -479,18 +485,36 @@ public class DozeParameters implements Settings.Secure.getUriFor(Settings.Secure.DOZE_ALWAYS_ON); private final Context mContext; - SettingsObserver(Context context, Handler handler) { + private final Handler mHandler; + private final SecureSettings mSecureSettings; + + SettingsObserver(Context context, Handler handler, SecureSettings secureSettings) { super(handler); mContext = context; + mHandler = handler; + mSecureSettings = secureSettings; } void observe() { - ContentResolver resolver = mContext.getContentResolver(); - resolver.registerContentObserver(mQuickPickupGesture, false, this, - UserHandle.USER_ALL); - resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL); - resolver.registerContentObserver(mAlwaysOnEnabled, false, this, UserHandle.USER_ALL); - update(null); + if (Flags.registerContentObserversAsync()) { + mSecureSettings.registerContentObserverForUserAsync(mQuickPickupGesture, + this, UserHandle.USER_ALL); + mSecureSettings.registerContentObserverForUserAsync(mPickupGesture, + this, UserHandle.USER_ALL); + mSecureSettings.registerContentObserverForUserAsync(mAlwaysOnEnabled, + this, UserHandle.USER_ALL, + // The register calls are called in order, so this ensures that update() + // is called after them all and value retrieval isn't racy. + () -> mHandler.post(() -> update(null))); + } else { + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(mQuickPickupGesture, false, this, + UserHandle.USER_ALL); + resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(mAlwaysOnEnabled, false, this, + UserHandle.USER_ALL); + update(null); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt index 3bf5b6511eb3..025354b51133 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -21,6 +21,7 @@ import android.database.ContentObserver import android.net.Uri import android.os.UserHandle import android.provider.Settings.SettingNotFoundException +import androidx.annotation.WorkerThread import com.android.app.tracing.TraceUtils.trace import com.android.systemui.settings.UserTracker import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat @@ -199,6 +200,24 @@ interface UserSettingsProxy : SettingsProxy { } /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * + * API corresponding to [registerContentObserverForUser] for Java usage. After registration is + * complete, the callback block is called on the <b>background thread</b> to allow for update of + * value. + */ + fun registerContentObserverForUserAsync( + uri: Uri, + settingsObserver: ContentObserver, + userHandle: Int, + @WorkerThread registered: Runnable + ) = + CoroutineScope(backgroundDispatcher).launch { + registerContentObserverForUserSync(uri, settingsObserver, userHandle) + registered.run() + } + + /** * Convenience wrapper around [ContentResolver.registerContentObserver] * * Implicitly calls [getUriFor] on the passed in name. diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt new file mode 100644 index 000000000000..14837f219862 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts +import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { + + private val fakeSystemSource = FakeKeyboardShortcutGroupsSource() + private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource() + + private val kosmos = + testKosmos().also { + it.testDispatcher = UnconfinedTestDispatcher() + it.shortcutHelperSystemShortcutsSource = fakeSystemSource + it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() + } + + private val repo = kosmos.shortcutHelperCategoriesRepository + private val helper = kosmos.shortcutHelperTestHelper + private val testScope = kosmos.testScope + + @Before + fun setUp() { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + } + + @Test + fun categories_multipleSubscribers_replaysExistingValueToNewSubscribers() = + testScope.runTest { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + helper.showFromActivity() + val firstCategories by collectLastValue(repo.categories) + + // Intentionally change shortcuts now. This simulates "current app" shortcuts changing + // when our helper is shown. + // We still want to return the shortcuts that were returned before our helper was + // showing. + fakeSystemSource.setGroups(emptyList()) + + val secondCategories by collectLastValue(repo.categories) + // Make sure the second subscriber receives the same value as the first subscriber, even + // though fetching shortcuts again would have returned a new result. + assertThat(secondCategories).isEqualTo(firstCategories) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt index d20ce3f1f0e9..57c8b444b922 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType. import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper @@ -48,14 +49,14 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { private val systemShortcutsSource = FakeKeyboardShortcutGroupsSource() private val multitaskingShortcutsSource = FakeKeyboardShortcutGroupsSource() - private val defaultAppsShortcutsSource = FakeKeyboardShortcutGroupsSource() @OptIn(ExperimentalCoroutinesApi::class) private val kosmos = testKosmos().also { it.testDispatcher = UnconfinedTestDispatcher() it.shortcutHelperSystemShortcutsSource = systemShortcutsSource it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource - it.shortcutHelperAppCategoriesShortcutsSource = defaultAppsShortcutsSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() } private val testScope = kosmos.testScope diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt index 0757ea156bbf..f8e2f47f939a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt @@ -20,8 +20,15 @@ import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter +import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity import com.android.systemui.kosmos.Kosmos @@ -32,6 +39,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -40,10 +48,18 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShortcutHelperActivityStarterTest : SysuiTestCase() { + private val fakeSystemSource = FakeKeyboardShortcutGroupsSource() + private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource() + private val kosmos = Kosmos().also { it.testCase = this it.testDispatcher = UnconfinedTestDispatcher() + it.shortcutHelperSystemShortcutsSource = fakeSystemSource + it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() } private val testScope = kosmos.testScope @@ -51,6 +67,12 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity private val starter = kosmos.shortcutHelperActivityStarter + @Before + fun setUp() { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + } + @Test fun start_doesNotStartByDefault() = testScope.runTest { @@ -70,13 +92,30 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { } @Test + fun start_onToggle_noShortcuts_doesNotStartActivity() = + testScope.runTest { + fakeSystemSource.setGroups(emptyList()) + fakeMultiTaskingSource.setGroups(emptyList()) + + starter.start() + + testHelper.toggle(deviceId = 456) + + assertThat(fakeStartActivity.startIntents).isEmpty() + } + + @Test fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() = testScope.runTest { starter.start() + // Starts testHelper.toggle(deviceId = 456) + // Stops testHelper.toggle(deviceId = 456) + // Starts again testHelper.toggle(deviceId = 456) + // Stops testHelper.toggle(deviceId = 456) verifyShortcutHelperActivityStarted(numTimes = 2) @@ -93,6 +132,18 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { } @Test + fun start_onRequestShowShortcuts_noShortcuts_doesNotStartActivity() = + testScope.runTest { + fakeSystemSource.setGroups(emptyList()) + fakeMultiTaskingSource.setGroups(emptyList()) + starter.start() + + testHelper.showFromActivity() + + assertThat(fakeStartActivity.startIntents).isEmpty() + } + + @Test fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() = testScope.runTest { starter.start() @@ -109,13 +160,21 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { testScope.runTest { starter.start() + // No-op. Already hidden. testHelper.hideFromActivity() + // No-op. Already hidden. testHelper.hideForSystem() + // Show 1st time. testHelper.toggle(deviceId = 987) + // No-op. Already shown. testHelper.showFromActivity() + // Hidden. testHelper.hideFromActivity() + // No-op. Already hidden. testHelper.hideForSystem() + // Show 2nd time. testHelper.toggle(deviceId = 456) + // No-op. Already shown. testHelper.showFromActivity() verifyShortcutHelperActivityStarted(numTimes = 2) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt index 80d487cab50d..07feaa1a5047 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt @@ -21,6 +21,13 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts +import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState @@ -34,6 +41,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -42,10 +50,18 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShortcutHelperViewModelTest : SysuiTestCase() { + private val fakeSystemSource = FakeKeyboardShortcutGroupsSource() + private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource() + private val kosmos = Kosmos().also { it.testCase = this it.testDispatcher = UnconfinedTestDispatcher() + it.shortcutHelperSystemShortcutsSource = fakeSystemSource + it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() } private val testScope = kosmos.testScope @@ -53,6 +69,12 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { private val sysUiState = kosmos.sysUiState private val viewModel = kosmos.shortcutHelperViewModel + @Before + fun setUp() { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + } + @Test fun shouldShow_falseByDefault() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 7cb41f119c9a..10d07a0ce004 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -35,8 +35,8 @@ import android.os.Handler; import android.os.PowerManager; import android.provider.Settings; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; @@ -52,6 +52,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; +import com.android.systemui.util.settings.FakeSettings; import org.junit.Assert; import org.junit.Before; @@ -130,7 +131,8 @@ public class DozeParametersTest extends SysuiTestCase { mConfigurationController, mStatusBarStateController, mUserTracker, - mDozeInteractor + mDozeInteractor, + new FakeSettings() ); verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index ef4e7341db74..cc2ef53c6cdb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -32,6 +33,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInteractor import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -126,6 +128,7 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) fun isVisible_headsUpStatusBarShown_false() = testScope.runTest { val latest by collectLastValue(underTest.isVisible) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt index e3e20c8ed501..5f7420d5a16b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt @@ -31,9 +31,11 @@ import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserTracker import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Before @@ -65,20 +67,21 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUser_inputString_success() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(false), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUser_inputString_success() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + TEST_SETTING, + mContentObserver, + mUserTracker.userId ) - } + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } @Test fun registerContentObserverForUserSuspend_inputString_success() = @@ -98,13 +101,14 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputString_success() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputString_success() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -113,24 +117,24 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } @Test - fun registerContentObserverForUser_inputString_notifyForDescendants_true() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(true), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUser_inputString_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + TEST_SETTING, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId ) - } + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(true), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } @Test fun registerContentObserverForUserSuspend_inputString_notifyForDescendants_true() = @@ -153,14 +157,15 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -169,23 +174,23 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } @Test - fun registerContentObserverForUser_inputUri_success() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING_URI, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(false), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUser_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + TEST_SETTING_URI, + mContentObserver, + mUserTracker.userId ) - } + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } @Test fun registerContentObserverForUserSuspend_inputUri_success() = @@ -205,13 +210,15 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputUri_success() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING_URI, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING_URI, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() + verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -220,24 +227,41 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun registerContentObserverForUser_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(true), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUserAsync_callbackAfterRegister() = + testScope.runTest { + var callbackCalled = false + val runnable = { callbackCalled = true } + + mSettings.registerContentObserverForUserAsync( + TEST_SETTING_URI, + mContentObserver, + mUserTracker.userId, + runnable ) - } + testScope.advanceUntilIdle() + assertThat(callbackCalled).isTrue() + } + + @Test + fun registerContentObserverForUser_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId + ) + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(true), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } @Test fun registerContentObserverForUserSuspend_inputUri_notifyForDescendants_true() = @@ -260,14 +284,15 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -276,14 +301,19 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } @Test - fun registerContentObserver_inputUri_success() { - mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver), eq(0)) - } + fun registerContentObserver_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver) + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(0) + ) + } @Test fun registerContentObserverSuspend_inputUri_success() = @@ -313,33 +343,26 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserver_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverSync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver - ) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver), eq(0)) - } - - @Test - fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() = + fun registerContentObserver_inputUri_notifyForDescendants_true() = testScope.runTest { - mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver) + mSettings.registerContentObserverSync( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver + ) verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), - eq(false), + eq(true), eq(mContentObserver), eq(0) ) } @Test - fun registerContentObserverAsync_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver) - testScope.launch { + fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver) verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -348,7 +371,21 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(0) ) } - } + + @Test + fun registerContentObserverAsync_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver) + testScope.launch { + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(0) + ) + } + } @Test fun getString_keyPresent_returnValidValue() { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 530df8aca442..001b55b99919 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -68,16 +68,17 @@ val Kosmos.shortcutHelperStateRepository by ) } -val Kosmos.shortcutHelperInputShortcutsSource by +var Kosmos.shortcutHelperInputShortcutsSource: KeyboardShortcutGroupsSource by Kosmos.Fixture { InputShortcutsSource(mainResources, windowManager) } -val Kosmos.shortcutHelperCurrentAppShortcutsSource by +var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) } val Kosmos.shortcutHelperCategoriesRepository by Kosmos.Fixture { ShortcutHelperCategoriesRepository( applicationContext, + applicationCoroutineScope, testDispatcher, shortcutHelperSystemShortcutsSource, shortcutHelperMultiTaskingShortcutsSource, @@ -96,7 +97,8 @@ val Kosmos.shortcutHelperTestHelper by applicationContext, broadcastDispatcher, fakeCommandQueue, - windowManager + fakeInputManager, + windowManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt index 40510db24f47..3e09b2379ff1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Intent +import android.hardware.input.FakeInputManager import android.view.KeyboardShortcutGroup import android.view.WindowManager import android.view.WindowManager.KeyboardShortcutsReceiver @@ -31,6 +32,7 @@ class ShortcutHelperTestHelper( private val context: Context, private val fakeBroadcastDispatcher: FakeBroadcastDispatcher, private val fakeCommandQueue: FakeCommandQueue, + private val fakeInputManager: FakeInputManager, windowManager: WindowManager ) { @@ -79,6 +81,7 @@ class ShortcutHelperTestHelper( } fun toggle(deviceId: Int) { + fakeInputManager.addPhysicalKeyboard(deviceId) fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) } } diff --git a/services/core/Android.bp b/services/core/Android.bp index 1cd20ed0f7cd..9d4310c21cf9 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -215,6 +215,7 @@ java_library_static { "power_hint_flags_lib", "biometrics_flags_lib", "am_flags_lib", + "updates_flags_lib", "com_android_server_accessibility_flags_lib", "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "com_android_wm_shell_flags_lib", diff --git a/services/core/java/com/android/server/updates/Android.bp b/services/core/java/com/android/server/updates/Android.bp new file mode 100644 index 000000000000..10beebb82711 --- /dev/null +++ b/services/core/java/com/android/server/updates/Android.bp @@ -0,0 +1,11 @@ +aconfig_declarations { + name: "updates_flags", + package: "com.android.server.updates", + container: "system", + srcs: ["*.aconfig"], +} + +java_aconfig_library { + name: "updates_flags_lib", + aconfig_declarations: "updates_flags", +} diff --git a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java index 5565b6ffb5ac..af4025e1db7c 100644 --- a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java +++ b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java @@ -16,17 +16,15 @@ package com.android.server.updates; +import android.content.Context; +import android.content.Intent; import android.os.FileUtils; import android.system.ErrnoException; import android.system.Os; -import android.util.Base64; import android.util.Slog; -import com.android.internal.util.HexDump; - import libcore.io.Streams; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -36,10 +34,7 @@ import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInstallReceiver { @@ -52,31 +47,31 @@ public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInsta @Override protected void install(InputStream inputStream, int version) throws IOException { - /* Install is complicated here because we translate the input, which is a JSON file - * containing log information to a directory with a file per log. To support atomically - * replacing the old configuration directory with the new there's a bunch of steps. We - * create a new directory with the logs and then do an atomic update of the current symlink - * to point to the new directory. - */ + if (!Flags.certificateTransparencyInstaller()) { + return; + } + // To support atomically replacing the old configuration directory with the new there's a + // bunch of steps. We create a new directory with the logs and then do an atomic update of + // the current symlink to point to the new directory. // 1. Ensure that the update dir exists and is readable updateDir.mkdir(); if (!updateDir.isDirectory()) { throw new IOException("Unable to make directory " + updateDir.getCanonicalPath()); } if (!updateDir.setReadable(true, false)) { - throw new IOException("Unable to set permissions on " + - updateDir.getCanonicalPath()); + throw new IOException("Unable to set permissions on " + updateDir.getCanonicalPath()); } File currentSymlink = new File(updateDir, "current"); File newVersion = new File(updateDir, LOGDIR_PREFIX + String.valueOf(version)); - File oldDirectory; // 2. Handle the corner case where the new directory already exists. if (newVersion.exists()) { // If the symlink has already been updated then the update died between steps 7 and 8 // and so we cannot delete the directory since its in use. Instead just bump the version // and return. if (newVersion.getCanonicalPath().equals(currentSymlink.getCanonicalPath())) { - writeUpdate(updateDir, updateVersion, + writeUpdate( + updateDir, + updateVersion, new ByteArrayInputStream(Long.toString(version).getBytes())); deleteOldLogDirectories(); return; @@ -91,22 +86,12 @@ public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInsta throw new IOException("Unable to make directory " + newVersion.getCanonicalPath()); } if (!newVersion.setReadable(true, false)) { - throw new IOException("Failed to set " +newVersion.getCanonicalPath() + - " readable"); + throw new IOException( + "Failed to set " + newVersion.getCanonicalPath() + " readable"); } - // 4. For each log in the log file create the corresponding file in <new_version>/ . - try { - byte[] content = Streams.readFullyNoClose(inputStream); - JSONObject json = new JSONObject(new String(content, StandardCharsets.UTF_8)); - JSONArray logs = json.getJSONArray("logs"); - for (int i = 0; i < logs.length(); i++) { - JSONObject log = logs.getJSONObject(i); - installLog(newVersion, log); - } - } catch (JSONException e) { - throw new IOException("Failed to parse logs", e); - } + // 4. Validate the log list json and move the file in <new_version>/ . + installLogList(newVersion, inputStream); // 5. Create the temp symlink. We'll rename this to the target symlink to get an atomic // update. @@ -125,62 +110,53 @@ public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInsta } Slog.i(TAG, "CT log directory updated to " + newVersion.getAbsolutePath()); // 7. Update the current version information - writeUpdate(updateDir, updateVersion, + writeUpdate( + updateDir, + updateVersion, new ByteArrayInputStream(Long.toString(version).getBytes())); // 8. Cleanup deleteOldLogDirectories(); } - private void installLog(File directory, JSONObject logObject) throws IOException { + @Override + protected void postInstall(Context context, Intent intent) { + if (!Flags.certificateTransparencyInstaller()) { + return; + } + } + + private void installLogList(File directory, InputStream inputStream) throws IOException { try { - String logFilename = getLogFileName(logObject.getString("key")); - File file = new File(directory, logFilename); - try (OutputStreamWriter out = - new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { - writeLogEntry(out, "key", logObject.getString("key")); - writeLogEntry(out, "url", logObject.getString("url")); - writeLogEntry(out, "description", logObject.getString("description")); + byte[] content = Streams.readFullyNoClose(inputStream); + if (new JSONObject(new String(content, StandardCharsets.UTF_8)).length() == 0) { + throw new IOException("Log list data not valid"); + } + + File file = new File(directory, "log_list.json"); + try (FileOutputStream outputStream = new FileOutputStream(file)) { + outputStream.write(content); } if (!file.setReadable(true, false)) { throw new IOException("Failed to set permissions on " + file.getCanonicalPath()); } } catch (JSONException e) { - throw new IOException("Failed to parse log", e); - } - - } - - /** - * Get the filename for a log based on its public key. This must be kept in sync with - * org.conscrypt.ct.CTLogStoreImpl. - */ - private String getLogFileName(String base64PublicKey) { - byte[] keyBytes = Base64.decode(base64PublicKey, Base64.DEFAULT); - try { - byte[] id = MessageDigest.getInstance("SHA-256").digest(keyBytes); - return HexDump.toHexString(id, false); - } catch (NoSuchAlgorithmException e) { - // SHA-256 is guaranteed to be available. - throw new RuntimeException(e); + throw new IOException("Malformed json in log list", e); } } - private void writeLogEntry(OutputStreamWriter out, String key, String value) - throws IOException { - out.write(key + ":" + value + "\n"); - } - private void deleteOldLogDirectories() throws IOException { if (!updateDir.exists()) { return; } File currentTarget = new File(updateDir, "current").getCanonicalFile(); - FileFilter filter = new FileFilter() { - @Override - public boolean accept(File file) { - return !currentTarget.equals(file) && file.getName().startsWith(LOGDIR_PREFIX); - } - }; + FileFilter filter = + new FileFilter() { + @Override + public boolean accept(File file) { + return !currentTarget.equals(file) + && file.getName().startsWith(LOGDIR_PREFIX); + } + }; for (File f : updateDir.listFiles(filter)) { FileUtils.deleteContentsAndDir(f); } diff --git a/services/core/java/com/android/server/updates/flags.aconfig b/services/core/java/com/android/server/updates/flags.aconfig new file mode 100644 index 000000000000..476cb3723c97 --- /dev/null +++ b/services/core/java/com/android/server/updates/flags.aconfig @@ -0,0 +1,10 @@ +package: "com.android.server.updates" +container: "system" + +flag { + name: "certificate_transparency_installer" + is_exported: true + namespace: "network_security" + description: "Enable certificate transparency installer for log list data" + bug: "319829948" +} diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 5be5bc5e3952..2c734127b7ea 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -47,7 +47,7 @@ import static com.android.server.accessibility.AccessibilityTraceProto.WHERE; import static com.android.server.accessibility.AccessibilityTraceProto.WINDOW_MANAGER_SERVICE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; -import static com.android.server.wm.WindowTracing.WINSCOPE_EXT; +import static com.android.server.wm.WindowTracingLegacy.WINSCOPE_EXT; import android.accessibilityservice.AccessibilityTrace; import android.animation.ObjectAnimator; diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java index ba5323ee1f8f..21f7eca5627a 100644 --- a/services/core/java/com/android/server/wm/WindowTracing.java +++ b/services/core/java/com/android/server/wm/WindowTracing.java @@ -18,89 +18,52 @@ package com.android.server.wm; import static android.os.Build.IS_USER; -import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; -import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS; import static com.android.server.wm.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS; import static com.android.server.wm.WindowManagerTraceProto.WHERE; import static com.android.server.wm.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE; import android.annotation.Nullable; import android.os.ShellCommand; -import android.os.SystemClock; import android.os.Trace; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.Choreographer; import com.android.internal.protolog.LegacyProtoLogImpl; -import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.ProtoLog; -import com.android.internal.util.TraceBuffer; -import java.io.File; -import java.io.IOException; import java.io.PrintWriter; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; /** - * A class that allows window manager to dump its state continuously to a trace file, such that a + * A class that allows window manager to dump its state continuously, such that a * time series of window manager state can be analyzed after the fact. */ -class WindowTracing { - - /** - * Maximum buffer size, currently defined as 5 MB - * Size was experimentally defined to fit between 100 to 150 elements. - */ - private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB - private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB - private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB - static final String WINSCOPE_EXT = ".winscope"; - private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT; - private static final String TAG = "WindowTracing"; - private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; +abstract class WindowTracing { + protected static final String TAG = "WindowTracing"; + protected static final String WHERE_START_TRACING = "trace.enable"; + protected static final String WHERE_ON_FRAME = "onFrame"; private final WindowManagerService mService; private final Choreographer mChoreographer; private final WindowManagerGlobalLock mGlobalLock; - private final Object mEnabledLock = new Object(); - private final File mTraceFile; - private final TraceBuffer mBuffer; private final Choreographer.FrameCallback mFrameCallback = (frameTimeNanos) -> - log("onFrame" /* where */); + log(WHERE_ON_FRAME); - private @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM; - private boolean mLogOnFrame = false; - private boolean mEnabled; - private volatile boolean mEnabledLockFree; - private boolean mScheduled; + private AtomicBoolean mScheduled = new AtomicBoolean(false); - private final IProtoLog mProtoLog; static WindowTracing createDefaultAndStartLooper(WindowManagerService service, Choreographer choreographer) { - File file = new File(TRACE_FILENAME); - return new WindowTracing(file, service, choreographer, BUFFER_CAPACITY_TRIM); + return new WindowTracingLegacy(service, choreographer); } - private WindowTracing(File file, WindowManagerService service, Choreographer choreographer, - int bufferCapacity) { - this(file, service, choreographer, service.mGlobalLock, bufferCapacity); - } - - WindowTracing(File file, WindowManagerService service, Choreographer choreographer, - WindowManagerGlobalLock globalLock, int bufferCapacity) { + protected WindowTracing(WindowManagerService service, Choreographer choreographer, + WindowManagerGlobalLock globalLock) { mChoreographer = choreographer; mService = service; mGlobalLock = globalLock; - mTraceFile = file; - mBuffer = new TraceBuffer(bufferCapacity); - setLogLevel(WindowTraceLogLevel.TRIM, null /* pw */); - mProtoLog = ProtoLog.getSingleInstance(); } void startTrace(@Nullable PrintWriter pw) { @@ -108,44 +71,29 @@ class WindowTracing { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } - synchronized (mEnabledLock) { - if (!android.tracing.Flags.perfettoProtologTracing()) { - ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw); - } - logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); - mBuffer.resetBuffer(); - mEnabled = mEnabledLockFree = true; + if (!android.tracing.Flags.perfettoProtologTracing()) { + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw); } - log("trace.enable"); + startTraceInternal(pw); } - /** - * Stops the trace and write the current buffer to disk - * @param pw Print writer - */ void stopTrace(@Nullable PrintWriter pw) { if (IS_USER) { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } - synchronized (mEnabledLock) { - logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); - mEnabled = mEnabledLockFree = false; - - if (mEnabled) { - logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush."); - throw new IllegalStateException("tracing enabled while waiting for flush."); - } - writeTraceToFileLocked(); - logAndPrintln(pw, "Trace written to " + mTraceFile + "."); - } if (!android.tracing.Flags.perfettoProtologTracing()) { ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true); } + stopTraceInternal(pw); } /** - * Stops the trace and write the current buffer to disk then restart, if it's already running. + * If legacy tracing is enabled (either WM or ProtoLog): + * 1. Stop tracing + * 2. Write trace to disk (to be picked by dumpstate) + * 3. Restart tracing + * * @param pw Print writer */ void saveForBugreport(@Nullable PrintWriter pw) { @@ -153,143 +101,24 @@ class WindowTracing { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } - synchronized (mEnabledLock) { - if (!mEnabled) { - return; - } - mEnabled = mEnabledLockFree = false; - logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); - writeTraceToFileLocked(); - logAndPrintln(pw, "Trace written to " + mTraceFile + "."); - if (!android.tracing.Flags.perfettoProtologTracing()) { - ((LegacyProtoLogImpl) mProtoLog).stopProtoLog(pw, true); - } - logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); - mBuffer.resetBuffer(); - mEnabled = mEnabledLockFree = true; - if (!android.tracing.Flags.perfettoProtologTracing()) { - ((LegacyProtoLogImpl) mProtoLog).startProtoLog(pw); - } - } - } - - private void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) { - logAndPrintln(pw, "Setting window tracing log level to " + logLevel); - mLogLevel = logLevel; - - switch (logLevel) { - case WindowTraceLogLevel.ALL: { - setBufferCapacity(BUFFER_CAPACITY_ALL, pw); - break; - } - case WindowTraceLogLevel.TRIM: { - setBufferCapacity(BUFFER_CAPACITY_TRIM, pw); - break; - } - case WindowTraceLogLevel.CRITICAL: { - setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw); - break; - } - } - } - - private void setLogFrequency(boolean onFrame, PrintWriter pw) { - logAndPrintln(pw, "Setting window tracing log frequency to " - + ((onFrame) ? "frame" : "transaction")); - mLogOnFrame = onFrame; - } - - private void setBufferCapacity(int capacity, PrintWriter pw) { - logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes"); - mBuffer.setCapacity(capacity); - } - - boolean isEnabled() { - return mEnabledLockFree; - } - - int onShellCommand(ShellCommand shell) { - PrintWriter pw = shell.getOutPrintWriter(); - String cmd = shell.getNextArgRequired(); - switch (cmd) { - case "start": - startTrace(pw); - return 0; - case "stop": - stopTrace(pw); - return 0; - case "save-for-bugreport": - saveForBugreport(pw); - return 0; - case "status": - logAndPrintln(pw, getStatus()); - return 0; - case "frame": - setLogFrequency(true /* onFrame */, pw); - mBuffer.resetBuffer(); - return 0; - case "transaction": - setLogFrequency(false /* onFrame */, pw); - mBuffer.resetBuffer(); - return 0; - case "level": - String logLevelStr = shell.getNextArgRequired().toLowerCase(); - switch (logLevelStr) { - case "all": { - setLogLevel(WindowTraceLogLevel.ALL, pw); - break; - } - case "trim": { - setLogLevel(WindowTraceLogLevel.TRIM, pw); - break; - } - case "critical": { - setLogLevel(WindowTraceLogLevel.CRITICAL, pw); - break; - } - default: { - setLogLevel(WindowTraceLogLevel.TRIM, pw); - break; - } - } - mBuffer.resetBuffer(); - return 0; - case "size": - setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw); - mBuffer.resetBuffer(); - return 0; - default: - pw.println("Unknown command: " + cmd); - pw.println("Window manager trace options:"); - pw.println(" start: Start logging"); - pw.println(" stop: Stop logging"); - pw.println(" save-for-bugreport: Save logging data to file if it's running."); - pw.println(" frame: Log trace once per frame"); - pw.println(" transaction: Log each transaction"); - pw.println(" size: Set the maximum log size (in KB)"); - pw.println(" status: Print trace status"); - pw.println(" level [lvl]: Set the log level between"); - pw.println(" lvl may be one of:"); - pw.println(" critical: Only visible windows with reduced information"); - pw.println(" trim: All windows with reduced"); - pw.println(" all: All window and information"); - return -1; + if (!android.tracing.Flags.perfettoProtologTracing() + && ProtoLog.getSingleInstance().isProtoEnabled()) { + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true); + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw); } + saveForBugreportInternal(pw); } - String getStatus() { - return "Status: " - + ((isEnabled()) ? "Enabled" : "Disabled") - + "\n" - + "Log level: " - + mLogLevel - + "\n" - + mBuffer.getStatus(); - } + abstract void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw); + abstract void setLogFrequency(boolean onFrame, PrintWriter pw); + abstract void setBufferCapacity(int capacity, PrintWriter pw); + abstract boolean isEnabled(); + abstract int onShellCommand(ShellCommand shell); + abstract String getStatus(); /** * If tracing is enabled, log the current state or schedule the next frame to be logged, - * according to {@link #mLogOnFrame}. + * according to the configuration in the derived tracing class. * * @param where Logging point descriptor */ @@ -298,59 +127,63 @@ class WindowTracing { return; } - if (mLogOnFrame) { - schedule(); - } else { + if (shouldLogOnTransaction()) { log(where); } + + if (shouldLogOnFrame()) { + schedule(); + } } /** * Schedule the log to trace the next frame */ private void schedule() { - if (mScheduled) { + if (!mScheduled.compareAndSet(false, true)) { return; } - mScheduled = true; mChoreographer.postFrameCallback(mFrameCallback); } /** - * Write the current frame to the buffer + * Write the current frame to proto * + * @param os Proto stream buffer + * @param logLevel Log level * @param where Logging point descriptor + * @param elapsedRealtimeNanos Timestamp */ - private void log(String where) { + protected void dumpToProto(ProtoOutputStream os, @WindowTraceLogLevel int logLevel, + String where, long elapsedRealtimeNanos) { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "traceStateLocked"); try { - ProtoOutputStream os = new ProtoOutputStream(); - long tokenOuter = os.start(ENTRY); - os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos()); + os.write(ELAPSED_REALTIME_NANOS, elapsedRealtimeNanos); os.write(WHERE, where); - long tokenInner = os.start(WINDOW_MANAGER_SERVICE); + long token = os.start(WINDOW_MANAGER_SERVICE); synchronized (mGlobalLock) { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked"); try { - mService.dumpDebugLocked(os, mLogLevel); + mService.dumpDebugLocked(os, logLevel); } finally { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } } - os.end(tokenInner); - os.end(tokenOuter); - mBuffer.add(os); - mScheduled = false; + os.end(token); } catch (Exception e) { Log.wtf(TAG, "Exception while tracing state", e); } finally { + boolean isOnFrameLogEvent = where == WHERE_ON_FRAME; + if (isOnFrameLogEvent) { + mScheduled.set(false); + } Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } } - private void logAndPrintln(@Nullable PrintWriter pw, String msg) { + protected void logAndPrintln(@Nullable PrintWriter pw, String msg) { Log.i(TAG, msg); if (pw != null) { pw.println(msg); @@ -358,24 +191,10 @@ class WindowTracing { } } - /** - * Writes the trace buffer to disk. This method has no internal synchronization and should be - * externally synchronized - */ - private void writeTraceToFileLocked() { - try { - Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked"); - ProtoOutputStream proto = new ProtoOutputStream(); - proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); - long timeOffsetNs = - TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) - - SystemClock.elapsedRealtimeNanos(); - proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs); - mBuffer.writeTraceToFile(mTraceFile, proto); - } catch (IOException e) { - Log.e(TAG, "Unable to write buffer to file", e); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); - } - } + protected abstract void startTraceInternal(@Nullable PrintWriter pw); + protected abstract void stopTraceInternal(@Nullable PrintWriter pw); + protected abstract void saveForBugreportInternal(@Nullable PrintWriter pw); + protected abstract void log(String where); + protected abstract boolean shouldLogOnFrame(); + protected abstract boolean shouldLogOnTransaction(); } diff --git a/services/core/java/com/android/server/wm/WindowTracingLegacy.java b/services/core/java/com/android/server/wm/WindowTracingLegacy.java new file mode 100644 index 000000000000..7a36707fd95a --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTracingLegacy.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; +import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS; + +import android.annotation.Nullable; +import android.os.ShellCommand; +import android.os.SystemClock; +import android.os.Trace; +import android.util.Log; +import android.util.proto.ProtoOutputStream; +import android.view.Choreographer; + +import com.android.internal.util.TraceBuffer; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.concurrent.TimeUnit; + +class WindowTracingLegacy extends WindowTracing { + + /** + * Maximum buffer size, currently defined as 5 MB + * Size was experimentally defined to fit between 100 to 150 elements. + */ + private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB + private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB + private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB + static final String WINSCOPE_EXT = ".winscope"; + private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT; + private static final String TAG = "WindowTracing"; + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + private final Object mEnabledLock = new Object(); + private final File mTraceFile; + private final TraceBuffer mBuffer; + + private boolean mEnabled; + private volatile boolean mEnabledLockFree; + + protected @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM; + protected boolean mLogOnFrame = false; + + WindowTracingLegacy(WindowManagerService service, Choreographer choreographer) { + this(new File(TRACE_FILENAME), service, choreographer, + service.mGlobalLock, BUFFER_CAPACITY_TRIM); + } + + WindowTracingLegacy(File traceFile, WindowManagerService service, Choreographer choreographer, + WindowManagerGlobalLock globalLock, int bufferSize) { + super(service, choreographer, globalLock); + mTraceFile = traceFile; + mBuffer = new TraceBuffer(bufferSize); + } + + @Override + void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) { + logAndPrintln(pw, "Setting window tracing log level to " + logLevel); + mLogLevel = logLevel; + + switch (logLevel) { + case WindowTraceLogLevel.ALL: { + setBufferCapacity(BUFFER_CAPACITY_ALL, pw); + break; + } + case WindowTraceLogLevel.TRIM: { + setBufferCapacity(BUFFER_CAPACITY_TRIM, pw); + break; + } + case WindowTraceLogLevel.CRITICAL: { + setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw); + break; + } + } + } + + @Override + void setLogFrequency(boolean onFrame, PrintWriter pw) { + logAndPrintln(pw, "Setting window tracing log frequency to " + + ((onFrame) ? "frame" : "transaction")); + mLogOnFrame = onFrame; + } + + @Override + void setBufferCapacity(int capacity, PrintWriter pw) { + logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes"); + mBuffer.setCapacity(capacity); + } + + @Override + boolean isEnabled() { + return mEnabledLockFree; + } + + @Override + int onShellCommand(ShellCommand shell) { + PrintWriter pw = shell.getOutPrintWriter(); + String cmd = shell.getNextArgRequired(); + switch (cmd) { + case "start": + startTrace(pw); + return 0; + case "stop": + stopTrace(pw); + return 0; + case "save-for-bugreport": + saveForBugreport(pw); + return 0; + case "status": + logAndPrintln(pw, getStatus()); + return 0; + case "frame": + setLogFrequency(true /* onFrame */, pw); + mBuffer.resetBuffer(); + return 0; + case "transaction": + setLogFrequency(false /* onFrame */, pw); + mBuffer.resetBuffer(); + return 0; + case "level": + String logLevelStr = shell.getNextArgRequired().toLowerCase(); + switch (logLevelStr) { + case "all": { + setLogLevel(WindowTraceLogLevel.ALL, pw); + break; + } + case "trim": { + setLogLevel(WindowTraceLogLevel.TRIM, pw); + break; + } + case "critical": { + setLogLevel(WindowTraceLogLevel.CRITICAL, pw); + break; + } + default: { + setLogLevel(WindowTraceLogLevel.TRIM, pw); + break; + } + } + mBuffer.resetBuffer(); + return 0; + case "size": + setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw); + mBuffer.resetBuffer(); + return 0; + default: + pw.println("Unknown command: " + cmd); + pw.println("Window manager trace options:"); + pw.println(" start: Start logging"); + pw.println(" stop: Stop logging"); + pw.println(" save-for-bugreport: Save logging data to file if it's running."); + pw.println(" frame: Log trace once per frame"); + pw.println(" transaction: Log each transaction"); + pw.println(" size: Set the maximum log size (in KB)"); + pw.println(" status: Print trace status"); + pw.println(" level [lvl]: Set the log level between"); + pw.println(" lvl may be one of:"); + pw.println(" critical: Only visible windows with reduced information"); + pw.println(" trim: All windows with reduced"); + pw.println(" all: All window and information"); + return -1; + } + } + + @Override + String getStatus() { + return "Status: " + + ((isEnabled()) ? "Enabled" : "Disabled") + + "\n" + + "Log level: " + + mLogLevel + + "\n" + + mBuffer.getStatus(); + } + + @Override + protected void startTraceInternal(@Nullable PrintWriter pw) { + synchronized (mEnabledLock) { + logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); + mBuffer.resetBuffer(); + mEnabled = mEnabledLockFree = true; + } + log(WHERE_START_TRACING); + } + + @Override + protected void stopTraceInternal(@Nullable PrintWriter pw) { + synchronized (mEnabledLock) { + logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); + mEnabled = mEnabledLockFree = false; + + if (mEnabled) { + logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush."); + throw new IllegalStateException("tracing enabled while waiting for flush."); + } + writeTraceToFileLocked(); + logAndPrintln(pw, "Trace written to " + mTraceFile + "."); + } + } + + @Override + protected void saveForBugreportInternal(@Nullable PrintWriter pw) { + synchronized (mEnabledLock) { + if (!mEnabled) { + return; + } + mEnabled = mEnabledLockFree = false; + logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); + writeTraceToFileLocked(); + logAndPrintln(pw, "Trace written to " + mTraceFile + "."); + logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); + mBuffer.resetBuffer(); + mEnabled = mEnabledLockFree = true; + } + } + + @Override + protected void log(String where) { + try { + ProtoOutputStream os = new ProtoOutputStream(); + long token = os.start(ENTRY); + dumpToProto(os, mLogLevel, where, SystemClock.elapsedRealtimeNanos()); + os.end(token); + mBuffer.add(os); + } catch (Exception e) { + Log.wtf(TAG, "Exception while tracing state", e); + } + } + + @Override + protected boolean shouldLogOnFrame() { + return mLogOnFrame; + } + + @Override + protected boolean shouldLogOnTransaction() { + return !mLogOnFrame; + } + + private void writeTraceToFileLocked() { + try { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked"); + ProtoOutputStream proto = new ProtoOutputStream(); + proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); + long timeOffsetNs = + TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) + - SystemClock.elapsedRealtimeNanos(); + proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs); + mBuffer.writeTraceToFile(mTraceFile, proto); + } catch (IOException e) { + Log.e(TAG, "Unable to write buffer to file", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java index c1834037f791..48a8d5502c64 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java @@ -63,7 +63,7 @@ import java.nio.charset.StandardCharsets; */ @SmallTest @Presubmit -public class WindowTracingTest { +public class WindowTracingLegacyTest { private static final byte[] MAGIC_HEADER = new byte[]{ 0x9, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45, @@ -88,7 +88,7 @@ public class WindowTracingTest { mFile = testContext.getFileStreamPath("tracing_test.dat"); mFile.delete(); - mWindowTracing = new WindowTracing(mFile, mWmMock, mChoreographer, + mWindowTracing = new WindowTracingLegacy(mFile, mWmMock, mChoreographer, new WindowManagerGlobalLock(), 1024); } |