diff options
45 files changed, 1955 insertions, 858 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 8a2616e70a6b..0ca97898e936 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -78,6 +78,7 @@ aconfig_declarations_group { "android.view.inputmethod.flags-aconfig-java", "android.webkit.flags-aconfig-java", "android.widget.flags-aconfig-java", + "art_exported_aconfig_flags_lib", "backstage_power_flags_lib", "backup_flags_lib", "camera_platform_flags_core_java_lib", @@ -140,6 +141,14 @@ java_defaults { libs: ["fake_device_config"], } +// ART +java_aconfig_library { + name: "art_exported_aconfig_flags_lib", + aconfig_declarations: "art-aconfig-flags", + mode: "exported", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Camera java_aconfig_library { name: "camera_platform_flags_core_java_lib", diff --git a/Android.bp b/Android.bp index 258440f24084..5b9f2cbf2d0d 100644 --- a/Android.bp +++ b/Android.bp @@ -427,6 +427,7 @@ java_defaults { "modules-utils-expresslog", "perfetto_trace_javastream_protos_jarjar", "libaconfig_java_proto_nano", + "aconfig_device_paths_java", ], } diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java index a87980c34f2d..e8ef8cd11585 100644 --- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java +++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java @@ -57,7 +57,7 @@ public abstract class VirtualInputDeviceConfig { mVendorId = builder.mVendorId; mProductId = builder.mProductId; mAssociatedDisplayId = builder.mAssociatedDisplayId; - mInputDeviceName = Objects.requireNonNull(builder.mInputDeviceName); + mInputDeviceName = Objects.requireNonNull(builder.mInputDeviceName, "Missing device name"); if (mAssociatedDisplayId == Display.INVALID_DISPLAY) { throw new IllegalArgumentException( @@ -77,7 +77,7 @@ public abstract class VirtualInputDeviceConfig { mVendorId = in.readInt(); mProductId = in.readInt(); mAssociatedDisplayId = in.readInt(); - mInputDeviceName = Objects.requireNonNull(in.readString8()); + mInputDeviceName = Objects.requireNonNull(in.readString8(), "Missing device name"); } /** diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java index 58ab5b6fd7ca..cfbf5289931d 100644 --- a/core/java/android/os/SystemVibratorManager.java +++ b/core/java/android/os/SystemVibratorManager.java @@ -138,11 +138,14 @@ public class SystemVibratorManager extends VibratorManager { Log.w(TAG, "Failed to vibrate; no vibrator manager service."); return; } + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason=" + reason); try { mService.vibrate(uid, mContext.getDeviceId(), opPkg, effect, attributes, reason, mToken); } catch (RemoteException e) { Log.w(TAG, "Failed to vibrate.", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -152,11 +155,14 @@ public class SystemVibratorManager extends VibratorManager { Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service."); return; } + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback, reason=" + reason); try { mService.performHapticFeedback(mUid, mContext.getDeviceId(), mPackageName, constant, reason, flags, privFlags); } catch (RemoteException e) { Log.w(TAG, "Failed to perform haptic feedback.", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -168,11 +174,15 @@ public class SystemVibratorManager extends VibratorManager { + " no vibrator manager service."); return; } + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, + "performHapticFeedbackForInputDevice, reason=" + reason); try { mService.performHapticFeedbackForInputDevice(mUid, mContext.getDeviceId(), mPackageName, constant, inputDeviceId, inputSource, reason, flags, privFlags); } catch (RemoteException e) { Log.w(TAG, "Failed to perform haptic feedback for input device.", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 6e89c497cbac..0f401d3e60b1 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -145,6 +145,13 @@ flag { } flag { + name: "enable_resizing_metrics" + namespace: "lse_desktop_experience" + description: "Whether to enable log collection for task resizing in desktop windowing mode" + bug: "341319100" +} + +flag { name: "enable_caption_compat_inset_force_consumption" namespace: "lse_desktop_experience" description: "Enables force-consumption of caption bar insets for immersive apps in freeform" diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index b873175451e1..39aadfb24b0c 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -18,6 +18,7 @@ package com.android.internal.pm.pkg.component; import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE; +import android.aconfig.DeviceProtos; import android.aconfig.nano.Aconfig; import android.aconfig.nano.Aconfig.parsed_flag; import android.aconfig.nano.Aconfig.parsed_flags; @@ -40,7 +41,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.util.List; +import java.util.Arrays; import java.util.Map; /** @@ -54,12 +55,6 @@ import java.util.Map; public class AconfigFlags { private static final String LOG_TAG = "AconfigFlags"; - private static final List<String> sTextProtoFilesOnDevice = List.of( - "/system/etc/aconfig_flags.pb", - "/system_ext/etc/aconfig_flags.pb", - "/product/etc/aconfig_flags.pb", - "/vendor/etc/aconfig_flags.pb"); - public enum Permission { READ_WRITE, READ_ONLY @@ -73,7 +68,10 @@ public class AconfigFlags { Slog.v(LOG_TAG, "Feature disabled, skipped all loading"); return; } - for (String fileName : sTextProtoFilesOnDevice) { + final var defaultFlagProtoFiles = + (Process.myUid() == Process.SYSTEM_UID) ? DeviceProtos.parsedFlagsProtoPaths() + : Arrays.asList(DeviceProtos.PATHS); + for (String fileName : defaultFlagProtoFiles) { try (var inputStream = new FileInputStream(fileName)) { loadAconfigDefaultValues(inputStream.readAllBytes()); } catch (IOException e) { diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_manage_windows.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_manage_windows.xml new file mode 100644 index 000000000000..7d912a24c443 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_manage_windows.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal"> + <path android:fillColor="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index eea3de8e30ca..64f71c713d1c 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -147,6 +147,14 @@ android:drawableStart="@drawable/desktop_mode_ic_handle_menu_new_window" android:drawableTint="?androidprv:attr/materialColorOnSurface" style="@style/DesktopModeHandleMenuActionButton" /> + + <Button + android:id="@+id/manage_windows_button" + android:contentDescription="@string/manage_windows_text" + android:text="@string/manage_windows_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_manage_windows" + android:drawableTint="?androidprv:attr/materialColorOnSurface" + style="@style/DesktopModeHandleMenuActionButton" /> </LinearLayout> <LinearLayout diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 755e0d5f742d..c76c47041ebf 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -504,9 +504,9 @@ <!-- The width of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_width">216dp</dimen> - <!-- The maximum height of the handle menu in desktop mode. Four pills (52dp each) plus 2dp - spacing between them plus 4dp top padding. --> - <dimen name="desktop_mode_handle_menu_height">270dp</dimen> + <!-- The maximum height of the handle menu in desktop mode. Three pills at 52dp each, + additional actions pill 156dp, plus 2dp spacing between them plus 4dp top padding. --> + <dimen name="desktop_mode_handle_menu_height">322dp</dimen> <!-- The elevation set on the handle menu pills. --> <dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen> @@ -520,6 +520,9 @@ <!-- The maximum height of the handle menu's "New Window" button in desktop mode. --> <dimen name="desktop_mode_handle_menu_new_window_height">52dp</dimen> + <!-- The maximum height of the handle menu's "Manage Windows" button in desktop mode. --> + <dimen name="desktop_mode_handle_menu_manage_windows_height">52dp</dimen> + <!-- The maximum height of the handle menu's "Screenshot" button in desktop mode. --> <dimen name="desktop_mode_handle_menu_screenshot_height">52dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index a353db72b914..a6da421dbbb9 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -294,6 +294,8 @@ <string name="open_in_browser_text">Open in browser</string> <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> <string name="new_window_text">New Window</string> + <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> + <string name="manage_windows_text">Manage Windows</string> <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] --> <string name="close_text">Close</string> <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt new file mode 100644 index 000000000000..79becb0a2e20 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt @@ -0,0 +1,173 @@ +/* + * 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.wm.shell.shared.desktopmode +import android.annotation.ColorInt +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.util.TypedValue +import android.view.MotionEvent.ACTION_OUTSIDE +import android.view.SurfaceView +import android.view.ViewGroup.MarginLayoutParams +import android.widget.LinearLayout +import android.window.TaskSnapshot + +/** + * View for the All Windows menu option, used by both Desktop Windowing and Taskbar. + * The menu displays icons of all open instances of an app. Clicking the icon should launch + * the instance, which will be performed by the child class. + */ +abstract class ManageWindowsViewContainer( + val context: Context, + @ColorInt private val menuBackgroundColor: Int +) { + lateinit var menuView: ManageWindowsView + + /** Creates the base menu view and fills it with icon views. */ + fun show(snapshotList: List<Pair<Int, TaskSnapshot>>, + onIconClickListener: ((Int) -> Unit), + onOutsideClickListener: (() -> Unit)): ManageWindowsView { + menuView = ManageWindowsView(context, menuBackgroundColor).apply { + this.onOutsideClickListener = onOutsideClickListener + this.onIconClickListener = onIconClickListener + this.generateIconViews(snapshotList) + } + addToContainer(menuView) + return menuView + } + + /** Adds the menu view to the container responsible for displaying it. */ + abstract fun addToContainer(menuView: ManageWindowsView) + + /** Dispose of the menu, perform needed cleanup. */ + abstract fun close() + + companion object { + const val MANAGE_WINDOWS_MINIMUM_INSTANCES = 2 + } + + class ManageWindowsView( + private val context: Context, + menuBackgroundColor: Int + ) { + val rootView: LinearLayout = LinearLayout(context) + var menuHeight = 0 + var menuWidth = 0 + var onIconClickListener: ((Int) -> Unit)? = null + var onOutsideClickListener: (() -> Unit)? = null + + init { + rootView.orientation = LinearLayout.VERTICAL + val menuBackground = ShapeDrawable() + val menuRadius = getDimensionPixelSize(MENU_RADIUS_DP) + menuBackground.shape = RoundRectShape( + FloatArray(8) { menuRadius }, + null, + null + ) + menuBackground.paint.color = menuBackgroundColor + rootView.background = menuBackground + rootView.elevation = getDimensionPixelSize(MENU_ELEVATION_DP) + rootView.setOnTouchListener { _, event -> + if (event.actionMasked == ACTION_OUTSIDE) { + onOutsideClickListener?.invoke() + } + return@setOnTouchListener true + } + } + + private fun getDimensionPixelSize(sizeDp: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + sizeDp, context.resources.displayMetrics) + } + + fun generateIconViews( + snapshotList: List<Pair<Int, TaskSnapshot>> + ) { + menuWidth = 0 + menuHeight = 0 + rootView.removeAllViews() + val instanceIconHeight = getDimensionPixelSize(ICON_HEIGHT_DP) + val instanceIconWidth = getDimensionPixelSize(ICON_WIDTH_DP) + val iconRadius = getDimensionPixelSize(ICON_RADIUS_DP) + val iconMargin = getDimensionPixelSize(ICON_MARGIN_DP) + var rowLayout: LinearLayout? = null + // Add each icon to the menu, adding a new row when needed. + for ((iconCount, taskInfoSnapshotPair) in snapshotList.withIndex()) { + val taskId = taskInfoSnapshotPair.first + val snapshot = taskInfoSnapshotPair.second + // Once a row is filled, make a new row and increase the menu height. + if (iconCount % MENU_MAX_ICONS_PER_ROW == 0) { + rowLayout = LinearLayout(context) + rowLayout.orientation = LinearLayout.HORIZONTAL + rootView.addView(rowLayout) + menuHeight += (instanceIconHeight + iconMargin).toInt() + } + val snapshotBitmap = Bitmap.wrapHardwareBuffer( + snapshot.hardwareBuffer, + snapshot.colorSpace + ) + val scaledSnapshotBitmap = snapshotBitmap?.let { + Bitmap.createScaledBitmap( + it, instanceIconWidth.toInt(), instanceIconHeight.toInt(), true /* filter */ + ) + } + val appSnapshotButton = SurfaceView(context) + appSnapshotButton.cornerRadius = iconRadius + appSnapshotButton.setZOrderOnTop(true) + appSnapshotButton.setOnClickListener { + onIconClickListener?.invoke(taskId) + } + val lp = MarginLayoutParams( + instanceIconWidth.toInt(), instanceIconHeight.toInt() + ) + lp.apply { + marginStart = iconMargin.toInt() + topMargin = iconMargin.toInt() + } + appSnapshotButton.layoutParams = lp + // If we haven't already reached one full row, increment width. + if (iconCount < MENU_MAX_ICONS_PER_ROW) { + menuWidth += (instanceIconWidth + iconMargin).toInt() + } + rowLayout?.addView(appSnapshotButton) + appSnapshotButton.requestLayout() + rowLayout?.post { + appSnapshotButton.holder.surface + .attachAndQueueBufferWithColorSpace( + scaledSnapshotBitmap?.hardwareBuffer, + scaledSnapshotBitmap?.colorSpace + ) + } + } + // Add margin again for the right/bottom of the menu. + menuWidth += iconMargin.toInt() + menuHeight += iconMargin.toInt() + } + + companion object { + private const val MENU_RADIUS_DP = 26f + private const val ICON_WIDTH_DP = 204f + private const val ICON_HEIGHT_DP = 127.5f + private const val ICON_RADIUS_DP = 16f + private const val ICON_MARGIN_DP = 16f + private const val MENU_ELEVATION_DP = 1f + private const val MENU_MAX_ICONS_PER_ROW = 3 + } + } +} 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 1d16980c617d..7e9625361efc 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 @@ -1077,45 +1077,47 @@ class DesktopTasksController( request.triggerTask != null } + /** Open an existing instance of an app. */ + fun openInstance( + callingTask: RunningTaskInfo, + requestedTaskId: Int + ) { + val wct = WindowContainerTransaction() + val options = createNewWindowOptions(callingTask) + if (options.launchWindowingMode == WINDOWING_MODE_FREEFORM) { + wct.startTask(requestedTaskId, options.toBundle()) + transitions.startTransition(TRANSIT_OPEN, wct, null) + } else { + val splitPosition = splitScreenController.determineNewInstancePosition(callingTask) + splitScreenController.startTask(requestedTaskId, splitPosition, + options.toBundle(), null /* hideTaskToken */) + } + } + + /** Create an Intent to open a new window of a task. */ fun openNewWindow( - taskInfo: RunningTaskInfo + callingTaskInfo: RunningTaskInfo ) { // TODO(b/337915660): Add a transition handler for these; animations // need updates in some cases. - val newTaskWindowingMode = when { - taskInfo.isFreeform -> { - WINDOWING_MODE_FREEFORM - } - taskInfo.isFullscreen || taskInfo.isMultiWindow -> { - WINDOWING_MODE_MULTI_WINDOW - } - else -> { - error("Invalid windowing mode: ${taskInfo.windowingMode}") - } - } - - val baseActivity = taskInfo.baseActivity ?: return + val baseActivity = callingTaskInfo.baseActivity ?: return val fillIn: Intent = context.packageManager .getLaunchIntentForPackage( baseActivity.packageName ) ?: return fillIn .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) - val options = - ActivityOptions.makeBasic().apply { - launchWindowingMode = newTaskWindowingMode - pendingIntentBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS - } val launchIntent = PendingIntent.getActivity( context, /* requestCode= */ 0, fillIn, PendingIntent.FLAG_IMMUTABLE ) - when (newTaskWindowingMode) { + val options = createNewWindowOptions(callingTaskInfo) + when (options.launchWindowingMode) { WINDOWING_MODE_MULTI_WINDOW -> { - val splitPosition = splitScreenController.determineNewInstancePosition(taskInfo) + val splitPosition = splitScreenController + .determineNewInstancePosition(callingTaskInfo) splitScreenController.startIntent( launchIntent, context.userId, fillIn, splitPosition, options.toBundle(), null /* hideTaskToken */ @@ -1130,6 +1132,25 @@ class DesktopTasksController( } } + private fun createNewWindowOptions(callingTask: RunningTaskInfo): ActivityOptions { + val newTaskWindowingMode = when { + callingTask.isFreeform -> { + WINDOWING_MODE_FREEFORM + } + callingTask.isFullscreen || callingTask.isMultiWindow -> { + WINDOWING_MODE_MULTI_WINDOW + } + else -> { + error("Invalid windowing mode: ${callingTask.windowingMode}") + } + } + return ActivityOptions.makeBasic().apply { + launchWindowingMode = newTaskWindowingMode + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS + } + } + /** * Handles the case where a freeform task is launched from recents. * diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt new file mode 100644 index 000000000000..13a805aef0f1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt @@ -0,0 +1,109 @@ +/* + * 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.wm.shell.windowdecor + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.view.WindowManager +import android.window.TaskSnapshot +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.android.wm.shell.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.extension.isFullscreen +import com.android.wm.shell.windowdecor.extension.isMultiWindow + +/** + * Implementation of [ManageWindowsViewContainer] meant to be used in desktop header and app + * handle. + */ +class DesktopHandleManageWindowsMenu( + private val callerTaskInfo: RunningTaskInfo, + private val splitScreenController: SplitScreenController, + private val captionX: Int, + private val captionWidth: Int, + private val windowManagerWrapper: WindowManagerWrapper, + context: Context, + snapshotList: List<Pair<Int, TaskSnapshot>>, + onIconClickListener: ((Int) -> Unit), + onOutsideClickListener: (() -> Unit) +) : ManageWindowsViewContainer( + context, + DecorThemeUtil(context).getColorScheme(callerTaskInfo).background.toArgb() +) { + private var menuViewContainer: AdditionalViewContainer? = null + + init { + show(snapshotList, onIconClickListener, onOutsideClickListener) + } + + override fun close() { + menuViewContainer?.releaseView() + } + + private fun calculateMenuPosition(): Point { + val position = Point() + val nonFreeformX = (captionX + (captionWidth / 2) - (menuView.menuWidth / 2)) + when { + callerTaskInfo.isFreeform -> { + val taskBounds = callerTaskInfo.getConfiguration().windowConfiguration.bounds + position.set(taskBounds.left, taskBounds.top) + } + callerTaskInfo.isFullscreen -> { + position.set(nonFreeformX, 0) + } + callerTaskInfo.isMultiWindow -> { + val splitPosition = splitScreenController.getSplitPosition(callerTaskInfo.taskId) + val leftOrTopStageBounds = Rect() + val rightOrBottomStageBounds = Rect() + splitScreenController.getStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) + // TODO(b/343561161): This needs to be calculated differently if the task is in + // top/bottom split. + when (splitPosition) { + SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { + position.set(leftOrTopStageBounds.width() + nonFreeformX, /* y= */ 0) + } + + SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { + position.set(nonFreeformX, /* y= */ 0) + } + } + } + } + return position + } + + override fun addToContainer(menuView: ManageWindowsView) { + val menuPosition = calculateMenuPosition() + menuViewContainer = AdditionalSystemViewContainer( + windowManagerWrapper, + callerTaskInfo.taskId, + menuPosition.x, + menuPosition.y, + menuView.menuWidth, + menuView.menuHeight, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + menuView.rootView + ) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt new file mode 100644 index 000000000000..05391a8343a5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt @@ -0,0 +1,104 @@ +/* + * 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.wm.shell.windowdecor + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.PixelFormat +import android.graphics.Point +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.WindowManager +import android.view.WindowlessWindowManager +import android.window.TaskConstants +import android.window.TaskSnapshot +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import java.util.function.Supplier + +/** + * Implementation of [ManageWindowsViewContainer] meant to be used in desktop header and app + * handle. + */ +class DesktopHeaderManageWindowsMenu( + private val callerTaskInfo: RunningTaskInfo, + private val displayController: DisplayController, + private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + context: Context, + private val surfaceControlBuilderSupplier: Supplier<SurfaceControl.Builder>, + private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>, + snapshotList: List<Pair<Int, TaskSnapshot>>, + onIconClickListener: ((Int) -> Unit), + onOutsideClickListener: (() -> Unit) +) : ManageWindowsViewContainer( + context, + DecorThemeUtil(context).getColorScheme(callerTaskInfo).background.toArgb() +) { + private var menuViewContainer: AdditionalViewContainer? = null + + init { + show(snapshotList, onIconClickListener, onOutsideClickListener) + } + + override fun close() { + menuViewContainer?.releaseView() + } + + override fun addToContainer(menuView: ManageWindowsView) { + val taskBounds = callerTaskInfo.getConfiguration().windowConfiguration.bounds + val menuPosition = Point(taskBounds.left, taskBounds.top) + val builder = surfaceControlBuilderSupplier.get() + rootTdaOrganizer.attachToDisplayArea(callerTaskInfo.displayId, builder) + val leash = builder + .setName("Manage Windows Menu") + .setContainerLayer() + .build() + val lp = WindowManager.LayoutParams( + menuView.menuWidth, menuView.menuHeight, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, + PixelFormat.TRANSPARENT + ) + val windowManager = WindowlessWindowManager( + callerTaskInfo.configuration, + leash, + null // HostInputToken + ) + val viewHost = SurfaceControlViewHost( + context, + displayController.getDisplay(callerTaskInfo.displayId), windowManager, + "MaximizeMenu" + ) + menuView.let { viewHost.setView(it.rootView, lp) } + val t = surfaceControlTransactionSupplier.get() + t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU) + .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat()) + .show(leash) + t.apply() + menuViewContainer = AdditionalViewHostViewContainer( + leash, viewHost, + surfaceControlTransactionSupplier + ) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 7692bd7b57e4..2519ce4e6571 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -39,6 +39,7 @@ import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.Indica import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; +import static com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer.MANAGE_WINDOWS_MINIMUM_INSTANCES; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; @@ -48,6 +49,8 @@ import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; +import android.app.IActivityManager; +import android.app.IActivityTaskManager; import android.content.Context; import android.content.Intent; import android.graphics.Point; @@ -77,6 +80,7 @@ import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.View; import android.widget.Toast; +import android.window.TaskSnapshot; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -125,9 +129,12 @@ import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; +import kotlin.Pair; import kotlin.Unit; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.function.Supplier; @@ -586,6 +593,61 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopTasksController.openNewWindow(decoration.mTaskInfo); } + private void onManageWindows(DesktopModeWindowDecoration decoration) { + if (decoration == null) { + return; + } + decoration.closeHandleMenu(); + decoration.createManageWindowsMenu(getTaskSnapshots(decoration.mTaskInfo), + /* onIconClickListener= */(Integer requestedTaskId) -> { + decoration.closeManageWindowsMenu(); + mDesktopTasksController.openInstance(decoration.mTaskInfo, requestedTaskId); + return Unit.INSTANCE; + }); + } + + private ArrayList<Pair<Integer, TaskSnapshot>> getTaskSnapshots( + @NonNull RunningTaskInfo callerTaskInfo + ) { + final ArrayList<Pair<Integer, TaskSnapshot>> snapshotList = new ArrayList<>(); + final IActivityManager activityManager = ActivityManager.getService(); + final IActivityTaskManager activityTaskManagerService = ActivityTaskManager.getService(); + final List<ActivityManager.RecentTaskInfo> recentTasks; + try { + recentTasks = mActivityTaskManager.getRecentTasks( + Integer.MAX_VALUE, + ActivityManager.RECENT_WITH_EXCLUDED, + activityManager.getCurrentUser().id); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + final String callerPackageName = callerTaskInfo.baseActivity.getPackageName(); + for (ActivityManager.RecentTaskInfo info : recentTasks) { + if (info.taskId == callerTaskInfo.taskId || info.baseActivity == null) continue; + final String infoPackageName = info.baseActivity.getPackageName(); + if (!infoPackageName.equals(callerPackageName)) { + continue; + } + if (info.baseActivity != null) { + if (callerPackageName.equals(infoPackageName)) { + // TODO(b/337903443): Fix this returning null for freeform tasks. + try { + TaskSnapshot screenshot = activityTaskManagerService + .getTaskSnapshot(info.taskId, false); + if (screenshot == null) { + screenshot = activityTaskManagerService + .takeTaskSnapshot(info.taskId, false); + } + snapshotList.add(new Pair(info.taskId, screenshot)); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + } + return snapshotList; + } + private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { @@ -642,7 +704,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); - decoration.createHandleMenu(); + decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) + >= MANAGE_WINDOWS_MINIMUM_INSTANCES); } } else if (id == R.id.maximize_window) { // TODO(b/346441962): move click detection logic into the decor's @@ -1350,6 +1413,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { onNewWindow(taskInfo.taskId); return Unit.INSTANCE; }); + windowDecoration.setManageWindowsClickListener(() -> { + onManageWindows(windowDecoration); + return Unit.INSTANCE; + }); windowDecoration.setCaptionListeners( touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); @@ -1441,6 +1508,29 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } + /** + * Gets the number of instances of a task running, not including the specified task itself. + */ + private int checkNumberOfOtherInstances(@NonNull RunningTaskInfo info) { + // TODO(b/336289597): Rather than returning number of instances, return a list of valid + // instances, then refer to the list's size and reuse the list for Manage Windows menu. + final IActivityTaskManager activityTaskManager = ActivityTaskManager.getService(); + final IActivityManager activityManager = ActivityManager.getService(); + try { + return activityTaskManager.getRecentTasks(Integer.MAX_VALUE, + ActivityManager.RECENT_WITH_EXCLUDED, + activityManager.getCurrentUserId()).getList().stream().filter( + recentTaskInfo -> (recentTaskInfo.taskId != info.taskId + && recentTaskInfo.baseActivity != null + && recentTaskInfo.baseActivity.getPackageName() + .equals(info.baseActivity.getPackageName()) + ) + ).toList().size(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + static class InputMonitorFactory { InputMonitor create(InputManager inputManager, int displayId) { return inputManager.monitorGestureInput("caption-touch", displayId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 1409d3011395..5d16d972a0f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -64,6 +64,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.ImageButton; +import android.window.TaskSnapshot; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; @@ -86,6 +87,7 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeFlags; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; @@ -93,9 +95,12 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder; import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; +import kotlin.Pair; import kotlin.Unit; import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; +import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; @@ -131,6 +136,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private Function0<Unit> mOnToFullscreenClickListener; private Function0<Unit> mOnToSplitscreenClickListener; private Function0<Unit> mOnNewWindowClickListener; + private Function0<Unit> mOnManageWindowsClickListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; private DragDetector mDragDetector; @@ -140,6 +146,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final Point mPositionInParent = new Point(); private HandleMenu mHandleMenu; + private boolean mMinimumInstancesFound; + private ManageWindowsViewContainer mManageWindowsMenu; private MaximizeMenu mMaximizeMenu; @@ -285,6 +293,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnNewWindowClickListener = listener; } + /** + * Registers a listener to be called when the decoration's manage windows action is + * triggered. + */ + void setManageWindowsClickListener(Function0<Unit> listener) { + mOnManageWindowsClickListener = listener; + } + void setCaptionListeners( View.OnClickListener onCaptionButtonClickListener, View.OnTouchListener onCaptionTouchListener, @@ -941,9 +957,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /** * Updates app info and creates and displays handle menu window. */ - void createHandleMenu() { + void createHandleMenu(boolean minimumInstancesFound) { // Requests assist content. When content is received, calls {@link #onAssistContentReceived} // which sets app info and creates the handle menu. + mMinimumInstancesFound = minimumInstancesFound; mAssistContentRequester.requestAssistContent( mTaskInfo.taskId, this::onAssistContentReceived); } @@ -956,8 +973,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWebUri = assistContent == null ? null : assistContent.getWebUri(); loadAppInfoIfNeeded(); updateGenericLink(); - - // Create and display handle menu + final boolean supportsMultiInstance = mMultiInstanceHelper + .supportsMultiInstanceSplit(mTaskInfo.baseActivity); + final boolean shouldShowManageWindowsButton = supportsMultiInstance + && mMinimumInstancesFound; mHandleMenu = mHandleMenuFactory.create( this, mWindowManagerWrapper, @@ -966,9 +985,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mAppName, mSplitScreenController, DesktopModeStatus.canEnterDesktopMode(mContext), - Flags.enableDesktopWindowingMultiInstanceFeatures() - && mMultiInstanceHelper - .supportsMultiInstanceSplit(mTaskInfo.baseActivity), + supportsMultiInstance, + shouldShowManageWindowsButton, getBrowserLink(), mResult.mCaptionWidth, mResult.mCaptionHeight, @@ -984,6 +1002,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /* onToFullscreenClickListener= */ mOnToFullscreenClickListener, /* onToSplitScreenClickListener= */ mOnToSplitscreenClickListener, /* onNewWindowClickListener= */ mOnNewWindowClickListener, + /* onManageWindowsClickListener= */ mOnManageWindowsClickListener, /* openInBrowserClickListener= */ (uri) -> { mOpenInBrowserClickListener.accept(uri); onCapturedLinkExpired(); @@ -998,6 +1017,47 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Unit.INSTANCE; } ); + mMinimumInstancesFound = false; + } + + void createManageWindowsMenu(@NonNull List<Pair<Integer, TaskSnapshot>> snapshotList, + @NonNull Function1<Integer, Unit> onIconClickListener + ) { + if (mTaskInfo.isFreeform()) { + mManageWindowsMenu = new DesktopHeaderManageWindowsMenu( + mTaskInfo, + mDisplayController, + mRootTaskDisplayAreaOrganizer, + mContext, + mSurfaceControlBuilderSupplier, + mSurfaceControlTransactionSupplier, + snapshotList, + onIconClickListener, + /* onOutsideClickListener= */ () -> { + closeManageWindowsMenu(); + return Unit.INSTANCE; + } + ); + } else { + mManageWindowsMenu = new DesktopHandleManageWindowsMenu( + mTaskInfo, + mSplitScreenController, + getCaptionX(), + mResult.mCaptionWidth, + mWindowManagerWrapper, + mContext, + snapshotList, + onIconClickListener, + /* onOutsideClickListener= */ () -> { + closeManageWindowsMenu(); + return Unit.INSTANCE; + } + ); + } + } + + void closeManageWindowsMenu() { + mManageWindowsMenu.close(); } private void updateGenericLink() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 748046ebd08d..3d00a445d9e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -71,6 +71,7 @@ class HandleMenu( private val splitScreenController: SplitScreenController, private val shouldShowWindowingPill: Boolean, private val shouldShowNewWindowButton: Boolean, + private val shouldShowManageWindowsButton: Boolean, private val openInBrowserLink: Uri?, private val captionWidth: Int, private val captionHeight: Int, @@ -119,6 +120,7 @@ class HandleMenu( onToFullscreenClickListener: () -> Unit, onToSplitScreenClickListener: () -> Unit, onNewWindowClickListener: () -> Unit, + onManageWindowsClickListener: () -> Unit, openInBrowserClickListener: (Uri) -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, @@ -133,6 +135,7 @@ class HandleMenu( onToFullscreenClickListener = onToFullscreenClickListener, onToSplitScreenClickListener = onToSplitScreenClickListener, onNewWindowClickListener = onNewWindowClickListener, + onManageWindowsClickListener = onManageWindowsClickListener, openInBrowserClickListener = openInBrowserClickListener, onCloseMenuClickListener = onCloseMenuClickListener, onOutsideTouchListener = onOutsideTouchListener, @@ -150,6 +153,7 @@ class HandleMenu( onToFullscreenClickListener: () -> Unit, onToSplitScreenClickListener: () -> Unit, onNewWindowClickListener: () -> Unit, + onManageWindowsClickListener: () -> Unit, openInBrowserClickListener: (Uri) -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit @@ -160,13 +164,15 @@ class HandleMenu( captionHeight = captionHeight, shouldShowWindowingPill = shouldShowWindowingPill, shouldShowBrowserPill = shouldShowBrowserPill, - shouldShowNewWindowButton = shouldShowNewWindowButton + shouldShowNewWindowButton = shouldShowNewWindowButton, + shouldShowManageWindowsButton = shouldShowManageWindowsButton ).apply { bind(taskInfo, appIconBitmap, appName) this.onToDesktopClickListener = onToDesktopClickListener this.onToFullscreenClickListener = onToFullscreenClickListener this.onToSplitScreenClickListener = onToSplitScreenClickListener this.onNewWindowClickListener = onNewWindowClickListener + this.onManageWindowsClickListener = onManageWindowsClickListener this.onOpenInBrowserClickListener = { openInBrowserClickListener.invoke(openInBrowserLink!!) } @@ -372,7 +378,13 @@ class HandleMenu( R.dimen.desktop_mode_handle_menu_new_window_height ) } - if (!SHOULD_SHOW_SCREENSHOT_BUTTON && !shouldShowNewWindowButton) { + if (!shouldShowManageWindowsButton) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_manage_windows_height + ) + } + if (!SHOULD_SHOW_SCREENSHOT_BUTTON && !shouldShowNewWindowButton + && !shouldShowManageWindowsButton) { menuHeight -= pillTopMargin } if (!shouldShowBrowserPill) { @@ -405,7 +417,8 @@ class HandleMenu( captionHeight: Int, private val shouldShowWindowingPill: Boolean, private val shouldShowBrowserPill: Boolean, - private val shouldShowNewWindowButton: Boolean + private val shouldShowNewWindowButton: Boolean, + private val shouldShowManageWindowsButton: Boolean ) { val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View @@ -430,6 +443,8 @@ class HandleMenu( private val moreActionsPill = rootView.requireViewById<View>(R.id.more_actions_pill) private val screenshotBtn = moreActionsPill.requireViewById<Button>(R.id.screenshot_button) private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button) + private val manageWindowBtn = moreActionsPill + .requireViewById<Button>(R.id.manage_windows_button) // Open in Browser Pill. private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill) @@ -446,6 +461,7 @@ class HandleMenu( var onToFullscreenClickListener: (() -> Unit)? = null var onToSplitScreenClickListener: (() -> Unit)? = null var onNewWindowClickListener: (() -> Unit)? = null + var onManageWindowsClickListener: (() -> Unit)? = null var onOpenInBrowserClickListener: (() -> Unit)? = null var onCloseMenuClickListener: (() -> Unit)? = null var onOutsideTouchListener: (() -> Unit)? = null @@ -457,6 +473,7 @@ class HandleMenu( browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() } collapseMenuButton.setOnClickListener { onCloseMenuClickListener?.invoke() } newWindowBtn.setOnClickListener { onNewWindowClickListener?.invoke() } + manageWindowBtn.setOnClickListener { onManageWindowsClickListener?.invoke() } rootView.setOnTouchListener { _, event -> if (event.actionMasked == ACTION_OUTSIDE) { @@ -587,6 +604,7 @@ class HandleMenu( private fun bindMoreActionsPill(style: MenuStyle) { moreActionsPill.apply { isGone = !shouldShowNewWindowButton && !SHOULD_SHOW_SCREENSHOT_BUTTON + && !shouldShowManageWindowsButton } screenshotBtn.apply { isGone = !SHOULD_SHOW_SCREENSHOT_BUTTON @@ -603,6 +621,13 @@ class HandleMenu( setTextColor(style.textColor) compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } + manageWindowBtn.apply { + isGone = !shouldShowManageWindowsButton + background.colorFilter = + BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + setTextColor(style.textColor) + compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + } } private fun bindOpenInBrowserPill(style: MenuStyle) { @@ -643,6 +668,7 @@ interface HandleMenuFactory { splitScreenController: SplitScreenController, shouldShowWindowingPill: Boolean, shouldShowNewWindowButton: Boolean, + shouldShowManageWindowsButton: Boolean, openInBrowserLink: Uri?, captionWidth: Int, captionHeight: Int, @@ -661,6 +687,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory { splitScreenController: SplitScreenController, shouldShowWindowingPill: Boolean, shouldShowNewWindowButton: Boolean, + shouldShowManageWindowsButton: Boolean, openInBrowserLink: Uri?, captionWidth: Int, captionHeight: Int, @@ -675,6 +702,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory { splitScreenController, shouldShowWindowingPill, shouldShowNewWindowButton, + shouldShowManageWindowsButton, openInBrowserLink, captionWidth, captionHeight, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 7b68ddf64fce..1741fe447fad 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -228,6 +228,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { mTestableContext = new TestableContext(mContext); mTestableContext.ensureTestableResources(); mContext.setMockPackageManager(mMockPackageManager); + when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())) + .thenReturn(false); when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel"); final ActivityInfo activityInfo = new ActivityInfo(); activityInfo.applicationInfo = new ApplicationInfo(); @@ -235,8 +237,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); - when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), - any(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt())) + when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(), + anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt())) .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false); when(mMockWindowDecorViewHostSupplier.acquire(any(), eq(defaultDisplay))) @@ -744,6 +746,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), openInBrowserCaptor.capture(), any(), any() @@ -771,6 +774,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), openInBrowserCaptor.capture(), any(), any() @@ -821,6 +825,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), closeClickListener.capture(), any() ); @@ -832,8 +837,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } private void verifyHandleMenuCreated(@Nullable Uri uri) { + verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(), - any(), anyBoolean(), anyBoolean(), eq(uri), anyInt(), anyInt(), anyInt()); + any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(), + anyInt(), anyInt()); } private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) { @@ -932,7 +939,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } private void createHandleMenu(@NonNull DesktopModeWindowDecoration decor) { - decor.createHandleMenu(); + decor.createHandleMenu(false); // Call DesktopModeWindowDecoration#onAssistContentReceived because decor waits to receive // {@link AssistContent} before creating the menu decor.onAssistContentReceived(mAssistContent); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index 100abbbb8332..a84523112d9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -236,11 +236,11 @@ class HandleMenuTest : ShellTestCase() { val handleMenu = HandleMenu(mockDesktopWindowDecoration, WindowManagerWrapper(mockWindowManager), layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true, - shouldShowNewWindowButton = true, + shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false, null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50, captionX = captionX ) - handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock()) + handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock()) return handleMenu } diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index b997c35668d2..0cb85d8638b0 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -42,8 +42,6 @@ android_library { "SettingsLibIllustrationPreference", "SettingsLibLayoutPreference", "SettingsLibMainSwitchPreference", - "SettingsLibMetadata", - "SettingsLibPreference", "SettingsLibProfileSelector", "SettingsLibProgressBar", "SettingsLibRestrictedLockUtils", diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java deleted file mode 100644 index 79949248cd8a..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settingslib.core.lifecycle; - - -import static androidx.lifecycle.Lifecycle.Event.ON_CREATE; -import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY; -import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; -import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; -import static androidx.lifecycle.Lifecycle.Event.ON_START; -import static androidx.lifecycle.Lifecycle.Event.ON_STOP; - -import android.annotation.CallSuper; -import android.content.Context; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import androidx.lifecycle.LifecycleOwner; -import androidx.preference.PreferenceScreen; - -import com.android.settingslib.preference.PreferenceFragment; - -/** - * Preference fragment that has hooks to observe fragment lifecycle events. - */ -public abstract class ObservablePreferenceFragment extends PreferenceFragment - implements LifecycleOwner { - - private final Lifecycle mLifecycle = new Lifecycle(this); - - public Lifecycle getSettingsLifecycle() { - return mLifecycle; - } - - @CallSuper - @Override - public void onAttach(Context context) { - super.onAttach(context); - mLifecycle.onAttach(context); - } - - @CallSuper - @Override - public void onCreate(Bundle savedInstanceState) { - mLifecycle.onCreate(savedInstanceState); - mLifecycle.handleLifecycleEvent(ON_CREATE); - super.onCreate(savedInstanceState); - } - - @Override - public void setPreferenceScreen(PreferenceScreen preferenceScreen) { - mLifecycle.setPreferenceScreen(preferenceScreen); - super.setPreferenceScreen(preferenceScreen); - } - - @CallSuper - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mLifecycle.onSaveInstanceState(outState); - } - - @CallSuper - @Override - public void onStart() { - mLifecycle.handleLifecycleEvent(ON_START); - super.onStart(); - } - - @CallSuper - @Override - public void onResume() { - mLifecycle.handleLifecycleEvent(ON_RESUME); - super.onResume(); - } - - @CallSuper - @Override - public void onPause() { - mLifecycle.handleLifecycleEvent(ON_PAUSE); - super.onPause(); - } - - @CallSuper - @Override - public void onStop() { - mLifecycle.handleLifecycleEvent(ON_STOP); - super.onStop(); - } - - @CallSuper - @Override - public void onDestroy() { - mLifecycle.handleLifecycleEvent(ON_DESTROY); - super.onDestroy(); - } - - @CallSuper - @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - mLifecycle.onCreateOptionsMenu(menu, inflater); - super.onCreateOptionsMenu(menu, inflater); - } - - @CallSuper - @Override - public void onPrepareOptionsMenu(final Menu menu) { - mLifecycle.onPrepareOptionsMenu(menu); - super.onPrepareOptionsMenu(menu); - } - - @CallSuper - @Override - public boolean onOptionsItemSelected(final MenuItem menuItem) { - boolean lifecycleHandled = mLifecycle.onOptionsItemSelected(menuItem); - if (!lifecycleHandled) { - return super.onOptionsItemSelected(menuItem); - } - return lifecycleHandled; - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 75ae4148d1df..b96e40f43318 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -244,10 +244,7 @@ class CommunalInteractorTest : SysuiTestCase() { val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) userRepository.setUserInfos(userInfos) - userTracker.set( - userInfos = userInfos, - selectedUserIndex = 0, - ) + userTracker.set(userInfos = userInfos, selectedUserIndex = 0) runCurrent() // Widgets available. @@ -267,21 +264,14 @@ class CommunalInteractorTest : SysuiTestCase() { fun smartspaceDynamicSizing_oneCard_fullSize() = testSmartspaceDynamicSizing( totalTargets = 1, - expectedSizes = - listOf( - CommunalContentSize.FULL, - ) + expectedSizes = listOf(CommunalContentSize.FULL), ) @Test fun smartspace_dynamicSizing_twoCards_halfSize() = testSmartspaceDynamicSizing( totalTargets = 2, - expectedSizes = - listOf( - CommunalContentSize.HALF, - CommunalContentSize.HALF, - ) + expectedSizes = listOf(CommunalContentSize.HALF, CommunalContentSize.HALF), ) @Test @@ -293,34 +283,34 @@ class CommunalInteractorTest : SysuiTestCase() { CommunalContentSize.THIRD, CommunalContentSize.THIRD, CommunalContentSize.THIRD, - ) + ), ) @Test - fun smartspace_dynamicSizing_fourCards_oneFullAndThreeThirdSize() = + fun smartspace_dynamicSizing_fourCards_threeThirdSizeAndOneFullSize() = testSmartspaceDynamicSizing( totalTargets = 4, expectedSizes = listOf( - CommunalContentSize.FULL, CommunalContentSize.THIRD, CommunalContentSize.THIRD, CommunalContentSize.THIRD, - ) + CommunalContentSize.FULL, + ), ) @Test - fun smartspace_dynamicSizing_fiveCards_twoHalfAndThreeThirdSize() = + fun smartspace_dynamicSizing_fiveCards_threeThirdAndTwoHalfSize() = testSmartspaceDynamicSizing( totalTargets = 5, expectedSizes = listOf( - CommunalContentSize.HALF, - CommunalContentSize.HALF, CommunalContentSize.THIRD, CommunalContentSize.THIRD, CommunalContentSize.THIRD, - ) + CommunalContentSize.HALF, + CommunalContentSize.HALF, + ), ) @Test @@ -335,7 +325,7 @@ class CommunalInteractorTest : SysuiTestCase() { CommunalContentSize.THIRD, CommunalContentSize.THIRD, CommunalContentSize.THIRD, - ) + ), ) private fun testSmartspaceDynamicSizing( @@ -355,7 +345,7 @@ class CommunalInteractorTest : SysuiTestCase() { smartspaceRepository.setTimers(targets) - val smartspaceContent by collectLastValue(underTest.ongoingContent) + val smartspaceContent by collectLastValue(underTest.ongoingContent(false)) assertThat(smartspaceContent?.size).isEqualTo(totalTargets) for (index in 0 until totalTargets) { assertThat(smartspaceContent?.get(index)?.size).isEqualTo(expectedSizes[index]) @@ -371,7 +361,7 @@ class CommunalInteractorTest : SysuiTestCase() { // Media is playing. mediaRepository.mediaActive() - val umoContent by collectLastValue(underTest.ongoingContent) + val umoContent by collectLastValue(underTest.ongoingContent(true)) assertThat(umoContent?.size).isEqualTo(1) assertThat(umoContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java) @@ -379,6 +369,19 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test + fun umo_mediaPlaying_mediaHostNotVisible_hidesUmo() = + testScope.runTest { + // Tutorial completed. + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + // Media is playing. + mediaRepository.mediaActive() + + val umoContent by collectLastValue(underTest.ongoingContent(false)) + assertThat(umoContent?.size).isEqualTo(0) + } + + @Test fun ongoing_shouldOrderAndSizeByTimestamp() = testScope.runTest { // Keyguard showing, and tutorial completed. @@ -401,19 +404,19 @@ class CommunalInteractorTest : SysuiTestCase() { val timer3 = smartspaceTimer("timer3", timestamp = 4L) smartspaceRepository.setTimers(listOf(timer1, timer2, timer3)) - val ongoingContent by collectLastValue(underTest.ongoingContent) + val ongoingContent by collectLastValue(underTest.ongoingContent(true)) assertThat(ongoingContent?.size).isEqualTo(4) assertThat(ongoingContent?.get(0)?.key) .isEqualTo(CommunalContentModel.KEY.smartspace("timer3")) - assertThat(ongoingContent?.get(0)?.size).isEqualTo(CommunalContentSize.FULL) + assertThat(ongoingContent?.get(0)?.size).isEqualTo(CommunalContentSize.HALF) assertThat(ongoingContent?.get(1)?.key) .isEqualTo(CommunalContentModel.KEY.smartspace("timer2")) - assertThat(ongoingContent?.get(1)?.size).isEqualTo(CommunalContentSize.THIRD) + assertThat(ongoingContent?.get(1)?.size).isEqualTo(CommunalContentSize.HALF) assertThat(ongoingContent?.get(2)?.key).isEqualTo(CommunalContentModel.KEY.umo()) - assertThat(ongoingContent?.get(2)?.size).isEqualTo(CommunalContentSize.THIRD) + assertThat(ongoingContent?.get(2)?.size).isEqualTo(CommunalContentSize.HALF) assertThat(ongoingContent?.get(3)?.key) .isEqualTo(CommunalContentModel.KEY.smartspace("timer1")) - assertThat(ongoingContent?.get(3)?.size).isEqualTo(CommunalContentSize.THIRD) + assertThat(ongoingContent?.get(3)?.size).isEqualTo(CommunalContentSize.HALF) } @Test @@ -435,10 +438,7 @@ class CommunalInteractorTest : SysuiTestCase() { testScope.runTest { // Set to main user, so we can dismiss the tile for the main user. val user = userRepository.asMainUser() - userTracker.set( - userInfos = listOf(user), - selectedUserIndex = 0, - ) + userTracker.set(userInfos = listOf(user), selectedUserIndex = 0) runCurrent() tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) @@ -816,10 +816,7 @@ class CommunalInteractorTest : SysuiTestCase() { // Only main user exists. val userInfos = listOf(MAIN_USER_INFO) userRepository.setUserInfos(userInfos) - userTracker.set( - userInfos = userInfos, - selectedUserIndex = 0, - ) + userTracker.set(userInfos = userInfos, selectedUserIndex = 0) runCurrent() val widgetContent by collectLastValue(underTest.widgetContent) @@ -853,10 +850,7 @@ class CommunalInteractorTest : SysuiTestCase() { // Work profile is set up. val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) userRepository.setUserInfos(userInfos) - userTracker.set( - userInfos = userInfos, - selectedUserIndex = 0, - ) + userTracker.set(userInfos = userInfos, selectedUserIndex = 0) runCurrent() // When work profile is paused. @@ -899,10 +893,7 @@ class CommunalInteractorTest : SysuiTestCase() { val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) userRepository.setUserInfos(userInfos) - userTracker.set( - userInfos = userInfos, - selectedUserIndex = 0, - ) + userTracker.set(userInfos = userInfos, selectedUserIndex = 0) userRepository.setSelectedUserInfo(MAIN_USER_INFO) runCurrent() @@ -914,7 +905,7 @@ class CommunalInteractorTest : SysuiTestCase() { setKeyguardFeaturesDisabled( USER_INFO_WORK, - DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL + DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL, ) // Widgets under work profile are filtered out. Only the regular widget remains. @@ -932,10 +923,7 @@ class CommunalInteractorTest : SysuiTestCase() { val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) userRepository.setUserInfos(userInfos) - userTracker.set( - userInfos = userInfos, - selectedUserIndex = 0, - ) + userTracker.set(userInfos = userInfos, selectedUserIndex = 0) userRepository.setSelectedUserInfo(MAIN_USER_INFO) runCurrent() @@ -947,7 +935,7 @@ class CommunalInteractorTest : SysuiTestCase() { setKeyguardFeaturesDisabled( USER_INFO_WORK, - DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE + DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE, ) // Widgets under work profile are available. @@ -967,7 +955,7 @@ class CommunalInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.OCCLUDED, - testScope + testScope, ) assertThat(showCommunalFromOccluded).isTrue() @@ -983,7 +971,7 @@ class CommunalInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.OCCLUDED, - testScope + testScope, ) assertThat(showCommunalFromOccluded).isFalse() @@ -999,7 +987,7 @@ class CommunalInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.OCCLUDED, - testScope + testScope, ) runCurrent() kosmos.setCommunalAvailable(false) @@ -1017,13 +1005,13 @@ class CommunalInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.OCCLUDED, - testScope + testScope, ) runCurrent() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.OCCLUDED, to = KeyguardState.PRIMARY_BOUNCER, - testScope + testScope, ) assertThat(showCommunalFromOccluded).isTrue() @@ -1039,7 +1027,7 @@ class CommunalInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.DREAMING, to = KeyguardState.OCCLUDED, - testScope + testScope, ) assertThat(showCommunalFromOccluded).isTrue() @@ -1049,7 +1037,7 @@ class CommunalInteractorTest : SysuiTestCase() { return CommunalSmartspaceTimer( smartspaceTargetId = id, createdTimestampMillis = timestamp, - remoteViews = mock(RemoteViews::class.java) + remoteViews = mock(RemoteViews::class.java), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 780d3576c5e4..09daa51a3b37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.communal.domain.interactor.communalSettingsInteracto import com.android.systemui.communal.domain.interactor.communalTutorialInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalMetricsLogger +import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS @@ -150,10 +151,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) - kosmos.fakeUserTracker.set( - userInfos = listOf(MAIN_USER_INFO), - selectedUserIndex = 0, - ) + kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) whenever(mediaHost.visible).thenReturn(true) kosmos.powerInteractor.setAwakeForTest() @@ -249,6 +247,87 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + fun ongoingContent_umoAndOneTimer_sizedAppropriately() = + testScope.runTest { + // Widgets available. + widgetRepository.addWidget(appWidgetId = 0, rank = 30) + widgetRepository.addWidget(appWidgetId = 1, rank = 20) + + // Smartspace available. + smartspaceRepository.setTimers( + listOf( + CommunalSmartspaceTimer( + smartspaceTargetId = "target", + createdTimestampMillis = 0L, + remoteViews = Mockito.mock(RemoteViews::class.java), + ) + ) + ) + + // Media playing. + mediaRepository.mediaActive() + + val communalContent by collectLastValue(underTest.communalContent) + + // One timer, UMO, two widgets, and cta. + assertThat(communalContent?.size).isEqualTo(5) + + val timer = communalContent?.get(0) + val umo = communalContent?.get(1) + + assertThat(timer).isInstanceOf(CommunalContentModel.Smartspace::class.java) + assertThat(umo).isInstanceOf(CommunalContentModel.Umo::class.java) + + assertThat(timer?.size).isEqualTo(CommunalContentSize.HALF) + assertThat(umo?.size).isEqualTo(CommunalContentSize.HALF) + } + + @Test + fun ongoingContent_umoAndTwoTimers_sizedAppropriately() = + testScope.runTest { + // Widgets available. + widgetRepository.addWidget(appWidgetId = 0, rank = 30) + widgetRepository.addWidget(appWidgetId = 1, rank = 20) + + // Smartspace available. + smartspaceRepository.setTimers( + listOf( + CommunalSmartspaceTimer( + smartspaceTargetId = "target", + createdTimestampMillis = 0L, + remoteViews = Mockito.mock(RemoteViews::class.java), + ), + CommunalSmartspaceTimer( + smartspaceTargetId = "target", + createdTimestampMillis = 0L, + remoteViews = Mockito.mock(RemoteViews::class.java), + ), + ) + ) + + // Media playing. + mediaRepository.mediaActive() + + val communalContent by collectLastValue(underTest.communalContent) + + // Two timers, UMO, two widgets, and cta. + assertThat(communalContent?.size).isEqualTo(6) + + val timer1 = communalContent?.get(0) + val timer2 = communalContent?.get(1) + val umo = communalContent?.get(2) + + assertThat(timer1).isInstanceOf(CommunalContentModel.Smartspace::class.java) + assertThat(timer2).isInstanceOf(CommunalContentModel.Smartspace::class.java) + assertThat(umo).isInstanceOf(CommunalContentModel.Umo::class.java) + + // One full-sized timer and a half-sized timer and half-sized UMO. + assertThat(timer1?.size).isEqualTo(CommunalContentSize.HALF) + assertThat(timer2?.size).isEqualTo(CommunalContentSize.HALF) + assertThat(umo?.size).isEqualTo(CommunalContentSize.FULL) + } + + @Test fun communalContent_mediaHostVisible_umoIncluded() = testScope.runTest { // Media playing. @@ -497,7 +576,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { TransitionStep( from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB, - ) + ), ) // Shade not expanded. if (!SceneContainerFlag.isEnabled) shadeTestUtil.setLockscreenShadeExpansion(0f) @@ -550,8 +629,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { stateTransition = TransitionStep( from = KeyguardState.DREAMING, - to = KeyguardState.GLANCEABLE_HUB, - ) + to = KeyguardState.GLANCEABLE_HUB + ), ) // Then flow is not frozen @@ -570,8 +649,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { stateTransition = TransitionStep( from = KeyguardState.GLANCEABLE_HUB, - to = KeyguardState.OCCLUDED, - ) + to = KeyguardState.OCCLUDED + ), ) // Then flow is not frozen @@ -595,7 +674,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { TransitionStep( from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB, - ) + ), ) // Then flow is not frozen @@ -614,7 +693,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { to = KeyguardState.OCCLUDED, transitionState = TransitionState.STARTED, value = 0f, - ) + ), ) // Then flow is frozen @@ -629,7 +708,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { to = KeyguardState.OCCLUDED, transitionState = TransitionState.FINISHED, value = 1f, - ) + ), ) // Then flow is not frozen @@ -658,8 +737,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { stateTransition = TransitionStep( from = KeyguardState.DREAMING, - to = KeyguardState.GLANCEABLE_HUB, - ) + to = KeyguardState.GLANCEABLE_HUB + ), ) // Widgets available diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index b570e14c646a..a687734ff499 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -117,7 +117,7 @@ constructor( sceneInteractor: SceneInteractor, @CommunalLog logBuffer: LogBuffer, @CommunalTableLog tableLogBuffer: TableLogBuffer, - private val managedProfileController: ManagedProfileController + private val managedProfileController: ManagedProfileController, ) { private val logger = Logger(logBuffer, "CommunalInteractor") @@ -154,7 +154,7 @@ constructor( allOf( communalSettingsInteractor.isCommunalEnabled, not(keyguardInteractor.isEncryptedOrLockdown), - keyguardInteractor.isKeyguardShowing + keyguardInteractor.isKeyguardShowing, ) .distinctUntilChanged() .onEach { available -> @@ -342,7 +342,7 @@ constructor( fun changeScene( newScene: SceneKey, loggingReason: String, - transitionKey: TransitionKey? = null + transitionKey: TransitionKey? = null, ) = communalSceneInteractor.changeScene(newScene, loggingReason, transitionKey) fun setEditModeOpen(isOpen: Boolean) { @@ -354,9 +354,7 @@ constructor( } /** Show the widget editor Activity. */ - fun showWidgetEditor( - shouldOpenWidgetPickerOnStart: Boolean = false, - ) { + fun showWidgetEditor(shouldOpenWidgetPickerOnStart: Boolean = false) { communalSceneInteractor.setEditModeState(EditModeState.STARTING) editWidgetsActivityStarter.startActivity(shouldOpenWidgetPickerOnStart) } @@ -419,7 +417,7 @@ constructor( IntentFilter().apply { addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) - }, + } ) .emitOnStart() @@ -450,7 +448,7 @@ constructor( rank = widget.rank, providerInfo = widget.providerInfo, appWidgetHost = appWidgetHost, - inQuietMode = isQuietModeEnabled(widget.providerInfo.profile) + inQuietMode = isQuietModeEnabled(widget.providerInfo.profile), ) } is CommunalWidgetContentModel.Pending -> { @@ -468,7 +466,7 @@ constructor( /** Filter widgets based on whether their associated profile is allowed by device policy. */ private fun filterWidgetsAllowedByDevicePolicy( list: List<CommunalWidgetContentModel>, - disallowedByDevicePolicyUser: UserInfo? + disallowedByDevicePolicyUser: UserInfo?, ): List<CommunalWidgetContentModel> = if (disallowedByDevicePolicyUser == null) { list @@ -507,7 +505,7 @@ constructor( * A flow of ongoing content, including smartspace timers and umo, ordered by creation time and * sized dynamically. */ - val ongoingContent: Flow<List<CommunalContentModel.Ongoing>> = + fun ongoingContent(isMediaHostVisible: Boolean): Flow<List<CommunalContentModel.Ongoing>> = combine(smartspaceRepository.timers, mediaRepository.mediaModel) { timers, media -> val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>() @@ -523,22 +521,20 @@ constructor( ) // Add UMO - if (media.hasAnyMediaOrRecommendation) { + if (isMediaHostVisible && media.hasAnyMediaOrRecommendation) { ongoingContent.add( CommunalContentModel.Umo( - createdTimestampMillis = media.createdTimestampMillis, + createdTimestampMillis = media.createdTimestampMillis ) ) } - // Order by creation time descending + // Order by creation time descending. ongoingContent.sortByDescending { it.createdTimestampMillis } + // Resize the items. + ongoingContent.resizeItems() - // Dynamic sizing - ongoingContent.forEachIndexed { index, model -> - model.size = dynamicContentSize(ongoingContent.size, index) - } - + // Return the sorted and resized items. ongoingContent } .flowOn(bgDispatcher) @@ -548,7 +544,7 @@ constructor( * stale data following user deletion. */ private fun filterWidgetsByExistingUsers( - list: List<CommunalWidgetContentModel>, + list: List<CommunalWidgetContentModel> ): List<CommunalWidgetContentModel> { val currentUserIds = userTracker.userProfiles.map { it.id }.toSet() return list.filter { widget -> @@ -560,6 +556,40 @@ constructor( } } + // Dynamically resizes the height of items in the list of ongoing items such that they fit in + // columns in as compact a space as possible. + // + // Currently there are three possible sizes. When the total number is 1, size for that content + // is [FULL], when the total number is 2, size for each is [HALF], and 3, size for each is + // [THIRD]. + // + // This algorithm also respects each item's minimum size. All items in a column will have the + // same size, and all items in a column will be no smaller than any item's minimum size. + private fun List<CommunalContentModel.Ongoing>.resizeItems() { + fun resizeColumn(c: List<CommunalContentModel.Ongoing>) { + if (c.isEmpty()) return + val newSize = CommunalContentSize.toSize(span = FULL.span / c.size) + c.forEach { item -> item.size = newSize } + } + + val column = mutableListOf<CommunalContentModel.Ongoing>() + var available = FULL.span + + forEach { item -> + if (available < item.minSize.span) { + resizeColumn(column) + column.clear() + available = FULL.span + } + + column.add(item) + available -= item.minSize.span + } + + // Make sure to resize the final column. + resizeColumn(column) + } + companion object { const val TAG = "CommunalInteractor" @@ -574,31 +604,6 @@ constructor( * of -1 means that the user's chosen screen timeout will be used instead. */ const val AWAKE_INTERVAL_MS = -1 - - /** - * Calculates the content size dynamically based on the total number of contents of that - * type. - * - * Contents with the same type are expected to fill each column evenly. Currently there are - * three possible sizes. When the total number is 1, size for that content is [FULL], when - * the total number is 2, size for each is [HALF], and 3, size for each is [THIRD]. - * - * When dynamic contents fill in multiple columns, the first column follows the algorithm - * above, and the remaining contents are packed in [THIRD]s. For example, when the total - * number if 4, the first one is [FULL], filling the column, and the remaining 3 are - * [THIRD]. - * - * @param size The total number of contents of this type. - * @param index The index of the current content of this type. - */ - private fun dynamicContentSize(size: Int, index: Int): CommunalContentSize { - val remainder = size % CommunalContentSize.entries.size - return CommunalContentSize.toSize( - span = - FULL.span / - if (index > remainder - 1) CommunalContentSize.entries.size else remainder - ) - } } /** diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 4c821d482eef..c2f6e85a33e4 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -34,12 +34,18 @@ sealed interface CommunalContentModel { /** Size to be rendered in the grid. */ val size: CommunalContentSize + /** The minimum size content can be resized to. */ + val minSize: CommunalContentSize + get() = CommunalContentSize.HALF + /** * A type of communal content is ongoing / live / ephemeral, and can be sized and ordered * dynamically. */ sealed interface Ongoing : CommunalContentModel { override var size: CommunalContentSize + override val minSize + get() = CommunalContentSize.THIRD /** Timestamp in milliseconds of when the content was created. */ val createdTimestampMillis: Long @@ -72,7 +78,7 @@ sealed interface CommunalContentModel { data class DisabledWidget( override val appWidgetId: Int, override val rank: Int, - val providerInfo: AppWidgetProviderInfo + val providerInfo: AppWidgetProviderInfo, ) : WidgetContent { override val key = KEY.disabledWidget(appWidgetId) override val componentName: ComponentName = providerInfo.provider @@ -109,10 +115,7 @@ sealed interface CommunalContentModel { override val size = CommunalContentSize.HALF } - class Tutorial( - id: Int, - override var size: CommunalContentSize, - ) : CommunalContentModel { + class Tutorial(id: Int, override var size: CommunalContentSize) : CommunalContentModel { override val key = KEY.tutorial(id) } @@ -128,6 +131,7 @@ sealed interface CommunalContentModel { class Umo( override val createdTimestampMillis: Long, override var size: CommunalContentSize = CommunalContentSize.HALF, + override var minSize: CommunalContentSize = CommunalContentSize.HALF, ) : Ongoing { override val key = KEY.umo() } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index d69ba1b23aa3..53109ac69fa9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -125,15 +125,9 @@ constructor( private var frozenCommunalContent: List<CommunalContentModel>? = null private val ongoingContent = - combine( - isMediaHostVisible, - communalInteractor.ongoingContent.onEach { mediaHost.updateViewVisibility() } - ) { mediaVisible, ongoingContent -> - if (mediaVisible) { - ongoingContent - } else { - // Media is not visible, don't show UMO - ongoingContent.filterNot { it is CommunalContentModel.Umo } + isMediaHostVisible.flatMapLatest { isMediaHostVisible -> + communalInteractor.ongoingContent(isMediaHostVisible).onEach { + mediaHost.updateViewVisibility() } } @@ -148,8 +142,7 @@ constructor( ongoingContent, communalInteractor.widgetContent, communalInteractor.ctaTileContent, - ) { ongoing, widgets, ctaTile, - -> + ) { ongoing, widgets, ctaTile -> ongoing + widgets + ctaTile } } @@ -172,10 +165,10 @@ constructor( allOf( keyguardTransitionInteractor.isFinishedIn( scene = Scenes.Communal, - stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB + stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB, ), keyguardInteractor.isKeyguardOccluded, - not(keyguardInteractor.isAbleToDream) + not(keyguardInteractor.isAbleToDream), ) .distinctUntilChanged() .onEach { logger.d("isCommunalContentFlowFrozen: $it") } @@ -208,7 +201,7 @@ constructor( combine( keyguardTransitionInteractor.isFinishedIn( scene = Scenes.Communal, - stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB + stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB, ), communalInteractor.isIdleOnCommunal, shadeInteractor.isAnyFullyExpanded, @@ -221,7 +214,7 @@ constructor( object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) // Hint user to long press in order to enter edit mode @@ -230,7 +223,7 @@ constructor( AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id, resources .getString(R.string.accessibility_action_label_edit_widgets) - .lowercase() + .lowercase(), ) ) } @@ -238,7 +231,7 @@ constructor( override fun performAccessibilityAction( host: View, action: Int, - args: Bundle? + args: Bundle?, ): Boolean { when (action) { AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id -> { @@ -271,9 +264,7 @@ constructor( } } - override fun onOpenWidgetEditor( - shouldOpenWidgetPickerOnStart: Boolean, - ) { + override fun onOpenWidgetEditor(shouldOpenWidgetPickerOnStart: Boolean) { persistScrollPosition() communalInteractor.showWidgetEditor(shouldOpenWidgetPickerOnStart) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt index 44aafabb103a..ad1a32e70a5b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt @@ -76,7 +76,7 @@ constructor( .filter { enabled -> !enabled } .sampleCombine( internalTransitionInteractor.currentTransitionInfoInternal, - biometricSettingsRepository.isCurrentUserInLockdown + biometricSettingsRepository.isCurrentUserInLockdown, ) .map { (_, transitionInfo, inLockdown) -> // ...we hide the keyguard, if it's showing and we're not in lockdown. In that case, @@ -91,12 +91,18 @@ constructor( */ scope.launch { if (!SceneContainerFlag.isEnabled) { - showKeyguardWhenReenabled - .filter { shouldDismiss -> shouldDismiss } - .collect { - keyguardDismissTransitionInteractor.startDismissKeyguardTransition( - "keyguard disabled" - ) + repository.isKeyguardEnabled + .filter { enabled -> !enabled } + .sampleCombine( + biometricSettingsRepository.isCurrentUserInLockdown, + internalTransitionInteractor.currentTransitionInfoInternal, + ) + .collect { (_, inLockdown, currentTransitionInfo) -> + if (currentTransitionInfo.to != KeyguardState.GONE && !inLockdown) { + keyguardDismissTransitionInteractor.startDismissKeyguardTransition( + "keyguard disabled" + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt index 420fbd4ae48d..7fd348b8b40e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt @@ -20,6 +20,7 @@ import android.os.DeadObjectException import android.os.RemoteException import com.android.internal.policy.IKeyguardStateCallback import com.android.systemui.CoreStartable +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -27,13 +28,13 @@ import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject /** * Updates KeyguardStateCallbacks provided to KeyguardService with KeyguardTransitionInteractor @@ -50,6 +51,8 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val selectedUserInteractor: SelectedUserInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val trustInteractor: TrustInteractor, + private val simBouncerInteractor: SimBouncerInteractor, ) : CoreStartable { private val callbacks = mutableListOf<IKeyguardStateCallback>() @@ -60,21 +63,19 @@ constructor( applicationScope.launch { combine( - selectedUserInteractor.selectedUser, - keyguardTransitionInteractor.currentKeyguardState, - ::Pair - ).collectLatest { (selectedUser, currentState) -> - val iterator = callbacks.iterator() + selectedUserInteractor.selectedUser, + keyguardTransitionInteractor.currentKeyguardState, + keyguardTransitionInteractor.startedKeyguardTransitionStep, + ::Triple, + ) + .collectLatest { (selectedUser, _, _) -> + val iterator = callbacks.iterator() withContext(backgroundDispatcher) { while (iterator.hasNext()) { val callback = iterator.next() try { - callback.onShowingStateChanged( - currentState != KeyguardState.GONE, - selectedUser - ) - callback.onInputRestrictedStateChanged( - currentState != KeyguardState.GONE) + callback.onShowingStateChanged(!isIdleInGone(), selectedUser) + callback.onInputRestrictedStateChanged(!isIdleInGone()) } catch (e: RemoteException) { if (e is DeadObjectException) { iterator.remove() @@ -84,10 +85,58 @@ constructor( } } } + + applicationScope.launch { + trustInteractor.isTrusted.collectLatest { isTrusted -> + val iterator = callbacks.iterator() + withContext(backgroundDispatcher) { + while (iterator.hasNext()) { + val callback = iterator.next() + try { + callback.onTrustedChanged(isTrusted) + } catch (e: RemoteException) { + if (e is DeadObjectException) { + iterator.remove() + } + } + } + } + } + } + + applicationScope.launch { + simBouncerInteractor.isAnySimSecure.collectLatest { isSimSecured -> + val iterator = callbacks.iterator() + withContext(backgroundDispatcher) { + while (iterator.hasNext()) { + val callback = iterator.next() + try { + callback.onSimSecureStateChanged(isSimSecured) + } catch (e: RemoteException) { + if (e is DeadObjectException) { + iterator.remove() + } + } + } + } + } + } } fun addCallback(callback: IKeyguardStateCallback) { KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode() callbacks.add(callback) + + // Send initial values to new callbacks. + callback.onShowingStateChanged(!isIdleInGone(), selectedUserInteractor.getSelectedUserId()) + callback.onInputRestrictedStateChanged(!isIdleInGone()) + callback.onTrustedChanged(trustInteractor.isTrusted.value) + callback.onSimSecureStateChanged(simBouncerInteractor.isAnySimSecure.value) + } + + /** Whether we're in KeyguardState.GONE and haven't started a transition to another state. */ + private fun isIdleInGone(): Boolean { + return keyguardTransitionInteractor.getCurrentState() == KeyguardState.GONE && + keyguardTransitionInteractor.getStartedState() == KeyguardState.GONE } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index e19b72e26567..c4f231dfc012 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -77,7 +77,7 @@ constructor( MutableSharedFlow<Float>( replay = 1, extraBufferCapacity = 2, - onBufferOverflow = BufferOverflow.DROP_OLDEST + onBufferOverflow = BufferOverflow.DROP_OLDEST, ) .also { it.tryEmit(0f) } } @@ -97,8 +97,8 @@ constructor( SharingStarted.Eagerly, WithPrev( sceneInteractor.transitionState.value, - sceneInteractor.transitionState.value - ) + sceneInteractor.transitionState.value, + ), ) /** @@ -156,7 +156,7 @@ constructor( Log.e( TAG, "STARTED step ($startedStep) was preceded by a RUNNING step " + - "($prevStep), which should never happen. Things could go badly here." + "($prevStep), which should never happen. Things could go badly here.", ) } } @@ -202,7 +202,7 @@ constructor( transitionMap.getOrPut(mappedEdge) { MutableSharedFlow( extraBufferCapacity = 10, - onBufferOverflow = BufferOverflow.DROP_OLDEST + onBufferOverflow = BufferOverflow.DROP_OLDEST, ) } @@ -262,7 +262,7 @@ constructor( is Edge.StateToState -> Edge.create( from = edge.from?.mapToSceneContainerState(), - to = edge.to?.mapToSceneContainerState() + to = edge.to?.mapToSceneContainerState(), ) is Edge.SceneToState -> Edge.create(UNDEFINED, edge.to) is Edge.StateToScene -> Edge.create(edge.from, UNDEFINED) @@ -286,9 +286,7 @@ constructor( * The value will be `0` (or close to `0`, due to float point arithmetic) if not in this step or * `1` when fully in the given state. */ - fun transitionValue( - state: KeyguardState, - ): Flow<Float> { + fun transitionValue(state: KeyguardState): Flow<Float> { if (SceneContainerFlag.isEnabled && state != state.mapToSceneContainerState()) { Log.e(TAG, "SceneContainer is enabled but a deprecated state $state is used.") return transitionValue(state.mapToSceneContainerScene()!!, state) @@ -369,10 +367,9 @@ constructor( .stateIn(scope, SharingStarted.Eagerly, OFF) val isInTransition = - combine( - isInTransitionWhere({ true }, { true }), - sceneInteractor.transitionState, - ) { isKeyguardTransitioning, sceneTransitionState -> + combine(isInTransitionWhere({ true }, { true }), sceneInteractor.transitionState) { + isKeyguardTransitioning, + sceneTransitionState -> isKeyguardTransitioning || (SceneContainerFlag.isEnabled && sceneTransitionState.isTransitioning()) } @@ -465,6 +462,10 @@ constructor( return currentKeyguardState.replayCache.last() } + fun getStartedState(): KeyguardState { + return startedKeyguardTransitionStep.value.to + } + private val finishedKeyguardState: StateFlow<KeyguardState> = repository.transitions .filter { it.transitionState == TransitionState.FINISHED } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index 4251b81226b3..d59658947771 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -53,6 +53,7 @@ import android.text.TextPaint; import android.text.TextUtils; import android.util.Log; import android.view.Window; +import android.view.WindowManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -308,6 +309,9 @@ public class MediaProjectionPermissionActivity extends Activity { private void setUpDialog(AlertDialog dialog) { SystemUIDialog.registerDismissListener(dialog); SystemUIDialog.applyFlags(dialog, /* showWhenLocked= */ false); + + final Window w = dialog.getWindow(); + w.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); SystemUIDialog.setDialogSize(dialog); dialog.setOnCancelListener(this::onDialogDismissedOrCancelled); @@ -315,7 +319,6 @@ public class MediaProjectionPermissionActivity extends Activity { dialog.create(); dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); - final Window w = dialog.getWindow(); w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt index 637cadde6fc6..920541d101cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt @@ -44,7 +44,7 @@ object FooterViewBinder { viewModel, clearAllNotifications, launchNotificationSettings, - launchNotificationHistory + launchNotificationHistory, ) } } @@ -55,21 +55,15 @@ object FooterViewBinder { viewModel: FooterViewModel, clearAllNotifications: View.OnClickListener, launchNotificationSettings: View.OnClickListener, - launchNotificationHistory: View.OnClickListener + launchNotificationHistory: View.OnClickListener, ) = coroutineScope { - launch { - bindClearAllButton( - footer, - viewModel, - clearAllNotifications, - ) - } + launch { bindClearAllButton(footer, viewModel, clearAllNotifications) } launch { bindManageOrHistoryButton( footer, viewModel, launchNotificationSettings, - launchNotificationHistory + launchNotificationHistory, ) } launch { bindMessage(footer, viewModel) } @@ -80,8 +74,6 @@ object FooterViewBinder { viewModel: FooterViewModel, clearAllNotifications: View.OnClickListener, ) = coroutineScope { - footer.setClearAllButtonClickListener(clearAllNotifications) - launch { viewModel.clearAllButton.labelId.collect { textId -> footer.setClearAllButtonText(textId) @@ -96,18 +88,21 @@ object FooterViewBinder { launch { viewModel.clearAllButton.isVisible.collect { isVisible -> + if (isVisible.value) { + footer.setClearAllButtonClickListener(clearAllNotifications) + } else { + // When the button isn't visible, it also shouldn't react to clicks. This is + // necessary because when the clear all button is not visible, it's actually + // just the alpha that becomes 0 so it can still be tapped. + footer.setClearAllButtonClickListener(null) + } + if (isVisible.isAnimating) { - footer.setClearAllButtonVisible( - isVisible.value, - /* animate = */ true, - ) { _ -> + footer.setClearAllButtonVisible(isVisible.value, /* animate= */ true) { _ -> isVisible.stopAnimating() } } else { - footer.setClearAllButtonVisible( - isVisible.value, - /* animate = */ false, - ) + footer.setClearAllButtonVisible(isVisible.value, /* animate= */ false) } } } @@ -143,22 +138,24 @@ object FooterViewBinder { launch { viewModel.manageOrHistoryButton.isVisible.collect { isVisible -> - // NOTE: This visibility change is never animated. + // NOTE: This visibility change is never animated. We also don't need to do anything + // special about the onClickListener here, since we're changing the visibility to + // GONE so it won't be clickable anyway. footer.setManageOrHistoryButtonVisible(isVisible.value) } } } - private suspend fun bindMessage( - footer: FooterView, - viewModel: FooterViewModel, - ) = coroutineScope { - // Bind the resource IDs - footer.setMessageString(viewModel.message.messageId) - footer.setMessageIcon(viewModel.message.iconId) + private suspend fun bindMessage(footer: FooterView, viewModel: FooterViewModel) = + coroutineScope { + // Bind the resource IDs + footer.setMessageString(viewModel.message.messageId) + footer.setMessageIcon(viewModel.message.iconId) - launch { - viewModel.message.isVisible.collect { visible -> footer.setFooterLabelVisible(visible) } + launch { + viewModel.message.isVisible.collect { visible -> + footer.setFooterLabelVisible(visible) + } + } } - } } diff --git a/services/core/java/com/android/server/cpu/CpuInfoReader.java b/services/core/java/com/android/server/cpu/CpuInfoReader.java index 984ad1dd7288..a68451aa1936 100644 --- a/services/core/java/com/android/server/cpu/CpuInfoReader.java +++ b/services/core/java/com/android/server/cpu/CpuInfoReader.java @@ -40,6 +40,7 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -80,13 +81,14 @@ public final class CpuInfoReader { /** package **/ @interface CpusetCategory{} // TODO(b/242722241): Protect updatable variables with a local lock. - private final File mCpusetDir; private final long mMinReadIntervalMillis; private final SparseIntArray mCpusetCategoriesByCpus = new SparseIntArray(); private final SparseArray<File> mCpuFreqPolicyDirsById = new SparseArray<>(); private final SparseArray<StaticPolicyInfo> mStaticPolicyInfoById = new SparseArray<>(); private final SparseArray<LongSparseLongArray> mTimeInStateByPolicyId = new SparseArray<>(); + private final AtomicBoolean mShouldReadCpusetCategories; + private File mCpusetDir; private File mCpuFreqDir; private File mProcStatFile; private SparseArray<CpuUsageStats> mCumulativeCpuUsageStats = new SparseArray<>(); @@ -106,10 +108,13 @@ public final class CpuInfoReader { mCpuFreqDir = cpuFreqDir; mProcStatFile = procStatFile; mMinReadIntervalMillis = minReadIntervalMillis; + mShouldReadCpusetCategories = new AtomicBoolean(true); } /** * Initializes CpuInfoReader and returns a boolean to indicate whether the reader is enabled. + * + * <p>Returns {@code true} on success. Otherwise, returns {@code false}. */ public boolean init() { if (mCpuFreqPolicyDirsById.size() > 0) { @@ -139,8 +144,7 @@ public final class CpuInfoReader { Slogf.e(TAG, "Missing proc stat file at %s", mProcStatFile.getAbsolutePath()); return false; } - readCpusetCategories(); - if (mCpusetCategoriesByCpus.size() == 0) { + if (!readCpusetCategories()) { Slogf.e(TAG, "Failed to read cpuset information from %s", mCpusetDir.getAbsolutePath()); return false; } @@ -163,10 +167,19 @@ public final class CpuInfoReader { return true; } + public void stopPeriodicCpusetReading() { + mShouldReadCpusetCategories.set(false); + if (!readCpusetCategories()) { + Slogf.e(TAG, "Failed to read cpuset information from %s", + mCpusetDir.getAbsolutePath()); + mIsEnabled = false; + } + } + /** * Reads CPU information from proc and sys fs files exposed by the Kernel. * - * @return SparseArray keyed by CPU core ID; {@code null} on error or when disabled. + * <p>Returns SparseArray keyed by CPU core ID; {@code null} on error or when disabled. */ @Nullable public SparseArray<CpuInfo> readCpuInfos() { @@ -183,6 +196,12 @@ public final class CpuInfoReader { } mLastReadUptimeMillis = uptimeMillis; mLastReadCpuInfos = null; + if (mShouldReadCpusetCategories.get() && !readCpusetCategories()) { + Slogf.e(TAG, "Failed to read cpuset information from %s", + mCpusetDir.getAbsolutePath()); + mIsEnabled = false; + return null; + } SparseArray<CpuUsageStats> cpuUsageStatsByCpus = readLatestCpuUsageStats(); if (cpuUsageStatsByCpus == null || cpuUsageStatsByCpus.size() == 0) { Slogf.e(TAG, "Failed to read latest CPU usage stats"); @@ -324,7 +343,7 @@ public final class CpuInfoReader { /** * Sets the CPU frequency for testing. * - * <p>Return {@code true} on success. Otherwise, returns {@code false}. + * <p>Returns {@code true} on success. Otherwise, returns {@code false}. */ @VisibleForTesting boolean setCpuFreqDir(File cpuFreqDir) { @@ -354,7 +373,7 @@ public final class CpuInfoReader { /** * Sets the proc stat file for testing. * - * <p>Return true on success. Otherwise, returns false. + * <p>Returns {@code true} on success. Otherwise, returns {@code false}. */ @VisibleForTesting boolean setProcStatFile(File procStatFile) { @@ -366,6 +385,21 @@ public final class CpuInfoReader { return true; } + /** + * Set the cpuset directory for testing. + * + * <p>Returns {@code true} on success. Otherwise, returns {@code false}. + */ + @VisibleForTesting + boolean setCpusetDir(File cpusetDir) { + if (!cpusetDir.exists() && !cpusetDir.isDirectory()) { + Slogf.e(TAG, "Missing or invalid cpuset directory at %s", cpusetDir.getAbsolutePath()); + return false; + } + mCpusetDir = cpusetDir; + return true; + } + private void populateCpuFreqPolicyDirsById(File[] policyDirs) { mCpuFreqPolicyDirsById.clear(); for (int i = 0; i < policyDirs.length; i++) { @@ -381,12 +415,27 @@ public final class CpuInfoReader { } } - private void readCpusetCategories() { + /** + * Reads cpuset categories by CPU. + * + * <p>The cpusets are read from the cpuset category specific directories + * under the /dev/cpuset directory. The cpuset categories are subject to change at any point + * during system bootup, as determined by the init rules specified within the init.rc files. + * Therefore, it's necessary to read the cpuset categories each time before accessing CPU usage + * statistics until the system boot completes. Once the boot is complete, the latest changes to + * the cpuset categories will take a few seconds to propagate. Thus, on boot complete, + * the periodic reading is stopped with a delay of + * {@link CpuMonitorService#STOP_PERIODIC_CPUSET_READING_DELAY_MILLISECONDS}. + * + * <p>Returns {@code true} on success. Otherwise, returns {@code false}. + */ + private boolean readCpusetCategories() { File[] cpusetDirs = mCpusetDir.listFiles(File::isDirectory); if (cpusetDirs == null) { Slogf.e(TAG, "Missing cpuset directories at %s", mCpusetDir.getAbsolutePath()); - return; + return false; } + mCpusetCategoriesByCpus.clear(); for (int i = 0; i < cpusetDirs.length; i++) { File dir = cpusetDirs[i]; @CpusetCategory int cpusetCategory; @@ -418,6 +467,7 @@ public final class CpuInfoReader { } } } + return mCpusetCategoriesByCpus.size() > 0; } private void readStaticPolicyInfo() { diff --git a/services/core/java/com/android/server/cpu/CpuMonitorService.java b/services/core/java/com/android/server/cpu/CpuMonitorService.java index 7ea2c1b02040..88ff7e4103f9 100644 --- a/services/core/java/com/android/server/cpu/CpuMonitorService.java +++ b/services/core/java/com/android/server/cpu/CpuMonitorService.java @@ -22,6 +22,7 @@ import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_ALL; import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACKGROUND; import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_BACKGROUND; import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_TOP_APP; +import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import android.annotation.Nullable; import android.content.Context; @@ -82,6 +83,15 @@ public final class CpuMonitorService extends SystemService { // frequently. Should this duration be increased as well when this happens? private static final long LATEST_AVAILABILITY_DURATION_MILLISECONDS = TimeUnit.SECONDS.toMillis(30); + /** + * Delay to stop the periodic cpuset reading after boot complete. + * + * Device specific implementations can update cpuset on boot complete. This may take + * a few seconds to propagate. So, wait for a few minutes before stopping the periodic cpuset + * reading. + */ + private static final long STOP_PERIODIC_CPUSET_READING_DELAY_MILLISECONDS = + TimeUnit.MINUTES.toMillis(2); private final Context mContext; private final HandlerThread mHandlerThread; @@ -90,6 +100,7 @@ public final class CpuMonitorService extends SystemService { private final long mNormalMonitoringIntervalMillis; private final long mDebugMonitoringIntervalMillis; private final long mLatestAvailabilityDurationMillis; + private final long mStopPeriodicCpusetReadingDelayMillis; private final Object mLock = new Object(); @GuardedBy("mLock") private final SparseArrayMap<CpuMonitorInternal.CpuAvailabilityCallback, @@ -153,13 +164,15 @@ public final class CpuMonitorService extends SystemService { this(context, new CpuInfoReader(), new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, /* allowIo= */ true), Build.IS_USERDEBUG || Build.IS_ENG, NORMAL_MONITORING_INTERVAL_MILLISECONDS, - DEBUG_MONITORING_INTERVAL_MILLISECONDS, LATEST_AVAILABILITY_DURATION_MILLISECONDS); + DEBUG_MONITORING_INTERVAL_MILLISECONDS, LATEST_AVAILABILITY_DURATION_MILLISECONDS, + STOP_PERIODIC_CPUSET_READING_DELAY_MILLISECONDS); } @VisibleForTesting CpuMonitorService(Context context, CpuInfoReader cpuInfoReader, HandlerThread handlerThread, boolean shouldDebugMonitor, long normalMonitoringIntervalMillis, - long debugMonitoringIntervalMillis, long latestAvailabilityDurationMillis) { + long debugMonitoringIntervalMillis, long latestAvailabilityDurationMillis, + long stopPeriodicCpusetReadingDelayMillis) { super(context); mContext = context; mHandlerThread = handlerThread; @@ -167,6 +180,7 @@ public final class CpuMonitorService extends SystemService { mNormalMonitoringIntervalMillis = normalMonitoringIntervalMillis; mDebugMonitoringIntervalMillis = debugMonitoringIntervalMillis; mLatestAvailabilityDurationMillis = latestAvailabilityDurationMillis; + mStopPeriodicCpusetReadingDelayMillis = stopPeriodicCpusetReadingDelayMillis; mCpuInfoReader = cpuInfoReader; mCpusetInfosByCpuset = new SparseArray<>(2); mCpusetInfosByCpuset.append(CPUSET_ALL, new CpusetInfo(CPUSET_ALL)); @@ -200,6 +214,16 @@ public final class CpuMonitorService extends SystemService { } } + @Override + public void onBootPhase(int phase) { + if (phase != PHASE_BOOT_COMPLETED) { + return; + } + Slogf.i(TAG, "Stopping periodic cpuset reading on boot complete"); + mHandler.postDelayed(() -> mCpuInfoReader.stopPeriodicCpusetReading(), + mStopPeriodicCpusetReadingDelayMillis); + } + @VisibleForTesting long getCurrentMonitoringIntervalMillis() { synchronized (mLock) { diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java index 2ed6f44f532e..67c3621b7c8c 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java +++ b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java @@ -39,35 +39,43 @@ public class TouchpadVisualizationView extends View { TouchpadHardwareState mLatestHardwareState = new TouchpadHardwareState(0, 0, 0, 0, new TouchpadFingerState[]{}); - private final Paint mOvalPaint; + private final Paint mOvalStrokePaint; + private final Paint mOvalFillPaint; + private final RectF mTempOvalRect = new RectF(); public TouchpadVisualizationView(Context context, TouchpadHardwareProperties touchpadHardwareProperties) { super(context); mTouchpadHardwareProperties = touchpadHardwareProperties; mScaleFactor = 1; - mOvalPaint = new Paint(); - mOvalPaint.setAntiAlias(true); - mOvalPaint.setARGB(255, 0, 0, 0); - mOvalPaint.setStyle(Paint.Style.STROKE); + mOvalStrokePaint = new Paint(); + mOvalStrokePaint.setAntiAlias(true); + mOvalStrokePaint.setARGB(255, 0, 0, 0); + mOvalStrokePaint.setStyle(Paint.Style.STROKE); + mOvalFillPaint = new Paint(); + mOvalFillPaint.setAntiAlias(true); + mOvalFillPaint.setARGB(255, 0, 0, 0); } - private final RectF mOvalRect = new RectF(); - - private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle, - Paint paint) { + private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle) { canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate(angle, x, y); - mOvalRect.left = x - minor / 2; - mOvalRect.right = x + minor / 2; - mOvalRect.top = y - major / 2; - mOvalRect.bottom = y + major / 2; - canvas.drawOval(mOvalRect, paint); + mTempOvalRect.left = x - minor / 2; + mTempOvalRect.right = x + minor / 2; + mTempOvalRect.top = y - major / 2; + mTempOvalRect.bottom = y + major / 2; + canvas.drawOval(mTempOvalRect, mOvalStrokePaint); + canvas.drawOval(mTempOvalRect, mOvalFillPaint); canvas.restore(); } @Override protected void onDraw(Canvas canvas) { + float maximumPressure = 0; + for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) { + maximumPressure = Math.max(maximumPressure, touchpadFingerState.getPressure()); + } + for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) { float newX = translateRange(mTouchpadHardwareProperties.getLeft(), mTouchpadHardwareProperties.getRight(), 0, getWidth(), @@ -88,7 +96,11 @@ public class TouchpadVisualizationView extends View { float newTouchMajor = touchpadFingerState.getTouchMajor() * mScaleFactor / resY; float newTouchMinor = touchpadFingerState.getTouchMinor() * mScaleFactor / resX; - drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle, mOvalPaint); + float pressureToOpacity = translateRange(0, maximumPressure, 0, 255, + touchpadFingerState.getPressure()); + mOvalFillPaint.setAlpha((int) pressureToOpacity); + + drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle); } } diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 4fa711262a08..82e00d9b4cbd 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -757,8 +757,12 @@ public class GroupHelper { // scenario 3: sparse/singleton groups if (Flags.notificationForceGroupSingletons()) { - groupSparseGroups(record, notificationList, summaryByGroupKey, sectioner, - fullAggregateGroupKey); + try { + groupSparseGroups(record, notificationList, summaryByGroupKey, sectioner, + fullAggregateGroupKey); + } catch (Throwable e) { + Slog.wtf(TAG, "Failed to group sparse groups", e); + } } } } diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java index 195e91cf5716..49825f16ca94 100644 --- a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java +++ b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java @@ -64,13 +64,36 @@ public interface UriGrantsManagerInternal { String targetPkg, int targetUserId); /** - * Same as {@link #checkGrantUriPermissionFromIntent(Intent, int, String, int)}, but with an - * extra parameter {@code requireContentUriPermissionFromCaller}, which is the value from {@link - * android.R.attr#requireContentUriPermissionFromCaller} attribute. + * Same as {@link #checkGrantUriPermissionFromIntent(Intent, int, String, int)}, but with: + * - {@code requireContentUriPermissionFromCaller}, which is the value from {@link + * android.R.attr#requireContentUriPermissionFromCaller} attribute. + * - {@code requestHashCode}, which is required to differentiate activity launches for logging + * ContentOrFileUriEventReported message. */ NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid, String targetPkg, int targetUserId, - @RequiredContentUriPermission int requireContentUriPermissionFromCaller); + @RequiredContentUriPermission int requireContentUriPermissionFromCaller, + int requestHashCode); + + /** + * Notify that an activity launch request has been completed and perform the following actions: + * - If the activity launch was unsuccessful, then clean up all the collected the content URIs + * that were passed during that launch. + * - If the activity launch was successful, then log cog content URIs that were passed during + * that launch. Specifically: + * - The caller didn't have read permission to them. + * - The activity's {@link android.R.attr#requireContentUriPermissionFromCaller} was set to + * "none". + * + * <p>Note that: + * - The API has to be called after + * {@link #checkGrantUriPermissionFromIntent(Intent, int, String, int, int, int)} was called. + * - The API is not idempotent, i.e. content URIs may be logged only once because the API clears + * the content URIs after logging. + */ + void notifyActivityLaunchRequestCompleted(int requestHashCode, boolean isSuccessfulLaunch, + String intentAction, int callingUid, String callingActivityName, int calleeUid, + String calleeActivityName, boolean isStartActivityForResult); /** * Extend a previously calculated set of permissions grants to the given diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java index a581b083f645..3479b6c926da 100644 --- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java +++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java @@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; import static android.content.pm.ActivityInfo.CONTENT_URI_PERMISSION_NONE; +import static android.content.pm.ActivityInfo.CONTENT_URI_PERMISSION_READ; import static android.content.pm.ActivityInfo.CONTENT_URI_PERMISSION_READ_OR_WRITE; import static android.content.pm.ActivityInfo.isRequiredContentUriPermissionRead; import static android.content.pm.ActivityInfo.isRequiredContentUriPermissionWrite; @@ -39,6 +40,8 @@ import static android.os.Process.SYSTEM_UID; import static android.os.Process.myUid; import static com.android.internal.util.XmlUtils.writeBooleanAttribute; +import static com.android.internal.util.FrameworkStatsLog.CONTENT_OR_FILE_URI_EVENT_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.CONTENT_OR_FILE_URI_EVENT_REPORTED__EVENT_TYPE__CONTENT_URI_WITHOUT_CALLER_READ_PERMISSION; import static com.android.server.uri.UriGrantsManagerService.H.PERSIST_URI_GRANTS_MSG; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; @@ -78,6 +81,7 @@ import android.os.UserHandle; import android.provider.Downloads; import android.text.format.DateUtils; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; @@ -86,6 +90,7 @@ import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.Preconditions; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -153,6 +158,22 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements private final SparseArray<ArrayMap<GrantUri, UriPermission>> mGrantedUriPermissions = new SparseArray<>(); + /** + * Global map of activity launches to sets of passed content URIs. Specifically: + * - The caller didn't have read permission to them. + * - The callee activity's {@link android.R.attr#requireContentUriPermissionFromCaller} was set + * to "none". + * + * <p>This map is used for logging the ContentOrFileUriEventReported message. + * + * <p>The launch id is the ActivityStarter.Request#hashCode and has to be received from + * ActivityStarter to {@link #checkGrantUriPermissionFromIntentUnlocked(int, String, Intent, + * int, NeededUriGrants, int, Integer, Integer)}. + */ + @GuardedBy("mLaunchToContentUrisWithoutCallerReadPermission") + private final SparseArray<ArraySet<Uri>> mLaunchToContentUrisWithoutCallerReadPermission = + new SparseArray<>(); + private UriGrantsManagerService() { this(SystemServiceManager.ensureSystemDir(), "uri-grants"); } @@ -613,7 +634,8 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements /** Like checkGrantUriPermission, but takes an Intent. */ private NeededUriGrants checkGrantUriPermissionFromIntentUnlocked(int callingUid, String targetPkg, Intent intent, int mode, NeededUriGrants needed, int targetUserId, - @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller) { + @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller, + Integer requestHashCode) { if (DEBUG) Slog.v(TAG, "Checking URI perm to data=" + (intent != null ? intent.getData() : null) + " clip=" + (intent != null ? intent.getClipData() : null) @@ -635,8 +657,9 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements } if (android.security.Flags.contentUriPermissionApis()) { - enforceRequireContentUriPermissionFromCallerOnIntentExtraStream(intent, contentUserHint, - mode, callingUid, requireContentUriPermissionFromCaller); + enforceRequireContentUriPermissionFromCallerOnIntentExtraStreamUnlocked(intent, + contentUserHint, mode, callingUid, requireContentUriPermissionFromCaller, + requestHashCode); } Uri data = intent.getData(); @@ -660,8 +683,9 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements if (data != null) { GrantUri grantUri = GrantUri.resolve(contentUserHint, data, mode); if (android.security.Flags.contentUriPermissionApis()) { - enforceRequireContentUriPermissionFromCaller(requireContentUriPermissionFromCaller, - grantUri, callingUid); + enforceRequireContentUriPermissionFromCallerUnlocked( + requireContentUriPermissionFromCaller, grantUri, callingUid, + requestHashCode); } targetUid = checkGrantUriPermissionUnlocked(callingUid, targetPkg, grantUri, mode, targetUid); @@ -678,8 +702,9 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements if (uri != null) { GrantUri grantUri = GrantUri.resolve(contentUserHint, uri, mode); if (android.security.Flags.contentUriPermissionApis()) { - enforceRequireContentUriPermissionFromCaller( - requireContentUriPermissionFromCaller, grantUri, callingUid); + enforceRequireContentUriPermissionFromCallerUnlocked( + requireContentUriPermissionFromCaller, grantUri, callingUid, + requestHashCode); } targetUid = checkGrantUriPermissionUnlocked(callingUid, targetPkg, grantUri, mode, targetUid); @@ -694,7 +719,7 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements if (clipIntent != null) { NeededUriGrants newNeeded = checkGrantUriPermissionFromIntentUnlocked( callingUid, targetPkg, clipIntent, mode, needed, targetUserId, - requireContentUriPermissionFromCaller); + requireContentUriPermissionFromCaller, requestHashCode); if (newNeeded != null) { needed = newNeeded; } @@ -706,17 +731,32 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements return needed; } - private void enforceRequireContentUriPermissionFromCaller( + private void enforceRequireContentUriPermissionFromCallerUnlocked( @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller, - GrantUri grantUri, int uid) { - // Ignore if requireContentUriPermissionFromCaller hasn't been set or the URI is a + GrantUri grantUri, int callingUid, Integer requestHashCode) { + // Exit early if requireContentUriPermissionFromCaller hasn't been set or the URI is a // non-content URI. if (requireContentUriPermissionFromCaller == null || requireContentUriPermissionFromCaller == CONTENT_URI_PERMISSION_NONE || !ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) { + tryAddingContentUriWithoutCallerReadPermissionWhenAttributeIsNoneUnlocked( + requireContentUriPermissionFromCaller, grantUri, callingUid, requestHashCode); return; } + final boolean hasPermission = hasRequireContentUriPermissionFromCallerUnlocked( + requireContentUriPermissionFromCaller, grantUri, callingUid); + + if (!hasPermission) { + throw new SecurityException("You can't launch this activity because you don't have the" + + " required " + ActivityInfo.requiredContentUriPermissionToShortString( + requireContentUriPermissionFromCaller) + " access to " + grantUri.uri); + } + } + + private boolean hasRequireContentUriPermissionFromCallerUnlocked( + @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller, + GrantUri grantUri, int uid) { final boolean readMet = !isRequiredContentUriPermissionRead( requireContentUriPermissionFromCaller) || checkContentUriPermissionFullUnlocked(grantUri, uid, @@ -727,26 +767,48 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements || checkContentUriPermissionFullUnlocked(grantUri, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - boolean hasPermission = - requireContentUriPermissionFromCaller == CONTENT_URI_PERMISSION_READ_OR_WRITE - ? (readMet || writeMet) : (readMet && writeMet); + return requireContentUriPermissionFromCaller == CONTENT_URI_PERMISSION_READ_OR_WRITE + ? (readMet || writeMet) : (readMet && writeMet); + } - if (!hasPermission) { - throw new SecurityException("You can't launch this activity because you don't have the" - + " required " + ActivityInfo.requiredContentUriPermissionToShortString( - requireContentUriPermissionFromCaller) + " access to " + grantUri.uri); + private void tryAddingContentUriWithoutCallerReadPermissionWhenAttributeIsNoneUnlocked( + @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller, + GrantUri grantUri, int callingUid, Integer requestHashCode) { + // We're interested in requireContentUriPermissionFromCaller that is set to + // CONTENT_URI_PERMISSION_NONE and content URIs. Hence, ignore if + // requireContentUriPermissionFromCaller is not set to CONTENT_URI_PERMISSION_NONE or the + // URI is a non-content URI. + if (requireContentUriPermissionFromCaller == null + || requireContentUriPermissionFromCaller != CONTENT_URI_PERMISSION_NONE + || !ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme()) + || requestHashCode == null) { + return; + } + + if (!hasRequireContentUriPermissionFromCallerUnlocked(CONTENT_URI_PERMISSION_READ, grantUri, + callingUid)) { + synchronized (mLaunchToContentUrisWithoutCallerReadPermission) { + if (mLaunchToContentUrisWithoutCallerReadPermission.get(requestHashCode) == null) { + mLaunchToContentUrisWithoutCallerReadPermission + .put(requestHashCode, new ArraySet<>()); + } + mLaunchToContentUrisWithoutCallerReadPermission.get(requestHashCode) + .add(grantUri.uri); + } } } - private void enforceRequireContentUriPermissionFromCallerOnIntentExtraStream(Intent intent, - int contentUserHint, int mode, int callingUid, - @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller) { + private void enforceRequireContentUriPermissionFromCallerOnIntentExtraStreamUnlocked( + Intent intent, int contentUserHint, int mode, int callingUid, + @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller, + Integer requestHashCode) { try { final Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class); if (uri != null) { final GrantUri grantUri = GrantUri.resolve(contentUserHint, uri, mode); - enforceRequireContentUriPermissionFromCaller( - requireContentUriPermissionFromCaller, grantUri, callingUid); + enforceRequireContentUriPermissionFromCallerUnlocked( + requireContentUriPermissionFromCaller, grantUri, callingUid, + requestHashCode); } } catch (BadParcelableException e) { Slog.w(TAG, "Failed to unparcel an URI in EXTRA_STREAM, skipping" @@ -759,8 +821,9 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements if (uris != null) { for (int i = uris.size() - 1; i >= 0; i--) { final GrantUri grantUri = GrantUri.resolve(contentUserHint, uris.get(i), mode); - enforceRequireContentUriPermissionFromCaller( - requireContentUriPermissionFromCaller, grantUri, callingUid); + enforceRequireContentUriPermissionFromCallerUnlocked( + requireContentUriPermissionFromCaller, grantUri, callingUid, + requestHashCode); } } } catch (BadParcelableException e) { @@ -769,6 +832,37 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements } } + private void notifyActivityLaunchRequestCompletedUnlocked(Integer requestHashCode, + boolean isSuccessfulLaunch, String intentAction, int callingUid, + String callingActivityName, int calleeUid, String calleeActivityName, + boolean isStartActivityForResult) { + ArraySet<Uri> contentUris; + synchronized (mLaunchToContentUrisWithoutCallerReadPermission) { + contentUris = mLaunchToContentUrisWithoutCallerReadPermission.get(requestHashCode); + mLaunchToContentUrisWithoutCallerReadPermission.remove(requestHashCode); + } + if (!isSuccessfulLaunch || contentUris == null) return; + + final String[] authorities = new String[contentUris.size()]; + final String[] schemes = new String[contentUris.size()]; + for (int i = contentUris.size() - 1; i >= 0; i--) { + Uri uri = contentUris.valueAt(i); + authorities[i] = uri.getAuthority(); + schemes[i] = uri.getScheme(); + } + FrameworkStatsLog.write(CONTENT_OR_FILE_URI_EVENT_REPORTED, + CONTENT_OR_FILE_URI_EVENT_REPORTED__EVENT_TYPE__CONTENT_URI_WITHOUT_CALLER_READ_PERMISSION, + intentAction, + callingUid, + callingActivityName, + calleeUid, + calleeActivityName, + isStartActivityForResult, + String.join(",", authorities), + String.join(",", schemes), + /* uri_mime_type */ null); + } + @GuardedBy("mLock") private void readGrantedUriPermissionsLocked() { if (DEBUG) Slog.v(TAG, "readGrantedUriPermissions()"); @@ -1645,23 +1739,36 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements public NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid, String targetPkg, int targetUserId) { return internalCheckGrantUriPermissionFromIntent(intent, callingUid, targetPkg, - targetUserId, /* requireContentUriPermissionFromCaller */ null); + targetUserId, /* requireContentUriPermissionFromCaller */ null, + /* requestHashCode */ null); } @Override public NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid, - String targetPkg, int targetUserId, int requireContentUriPermissionFromCaller) { + String targetPkg, int targetUserId, int requireContentUriPermissionFromCaller, + int requestHashCode) { return internalCheckGrantUriPermissionFromIntent(intent, callingUid, targetPkg, - targetUserId, requireContentUriPermissionFromCaller); + targetUserId, requireContentUriPermissionFromCaller, requestHashCode); + } + + @Override + public void notifyActivityLaunchRequestCompleted(int requestHashCode, + boolean isSuccessfulLaunch, String intentAction, int callingUid, + String callingActivityName, int calleeUid, String calleeActivityName, + boolean isStartActivityForResult) { + UriGrantsManagerService.this.notifyActivityLaunchRequestCompletedUnlocked( + requestHashCode, isSuccessfulLaunch, intentAction, callingUid, + callingActivityName, calleeUid, calleeActivityName, + isStartActivityForResult); } private NeededUriGrants internalCheckGrantUriPermissionFromIntent(Intent intent, int callingUid, String targetPkg, int targetUserId, - @Nullable Integer requireContentUriPermissionFromCaller) { + @Nullable Integer requireContentUriPermissionFromCaller, Integer requestHashCode) { final int mode = (intent != null) ? intent.getFlags() : 0; return UriGrantsManagerService.this.checkGrantUriPermissionFromIntentUnlocked( callingUid, targetPkg, intent, mode, null, targetUserId, - requireContentUriPermissionFromCaller); + requireContentUriPermissionFromCaller, requestHashCode); } @Override diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index ab4a4d8fc08d..4c1e16c0d14e 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -128,15 +128,20 @@ final class VibrationThread extends Thread { * before the release callback. */ boolean runVibrationOnVibrationThread(VibrationStepConductor conductor) { - synchronized (mLock) { - if (mRequestedActiveConductor != null) { - Slog.wtf(TAG, "Attempt to start vibration when one already running"); - return false; + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread"); + try { + synchronized (mLock) { + if (mRequestedActiveConductor != null) { + Slog.wtf(TAG, "Attempt to start vibration when one already running"); + return false; + } + mRequestedActiveConductor = conductor; + mLock.notifyAll(); } - mRequestedActiveConductor = conductor; - mLock.notifyAll(); + return true; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } - return true; } @Override diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index 4fc0b74ecb80..3c478500876f 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -23,6 +23,7 @@ import android.os.IVibratorStateListener; import android.os.Parcel; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.os.Trace; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; @@ -123,21 +124,26 @@ final class VibratorController { /** Reruns the query to the vibrator to load the {@link VibratorInfo}, if not yet successful. */ public void reloadVibratorInfoIfNeeded() { - // Early check outside lock, for quick return. - if (mVibratorInfoLoadSuccessful) { - return; - } - synchronized (mLock) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded"); + try { + // Early check outside lock, for quick return. if (mVibratorInfoLoadSuccessful) { return; } - int vibratorId = mVibratorInfo.getId(); - VibratorInfo.Builder vibratorInfoBuilder = new VibratorInfo.Builder(vibratorId); - mVibratorInfoLoadSuccessful = mNativeWrapper.getInfo(vibratorInfoBuilder); - mVibratorInfo = vibratorInfoBuilder.build(); - if (!mVibratorInfoLoadSuccessful) { - Slog.e(TAG, "Failed retry of HAL getInfo for vibrator " + vibratorId); + synchronized (mLock) { + if (mVibratorInfoLoadSuccessful) { + return; + } + int vibratorId = mVibratorInfo.getId(); + VibratorInfo.Builder vibratorInfoBuilder = new VibratorInfo.Builder(vibratorId); + mVibratorInfoLoadSuccessful = mNativeWrapper.getInfo(vibratorInfoBuilder); + mVibratorInfo = vibratorInfoBuilder.build(); + if (!mVibratorInfoLoadSuccessful) { + Slog.e(TAG, "Failed retry of HAL getInfo for vibrator " + vibratorId); + } } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -193,8 +199,13 @@ final class VibratorController { /** Return {@code true} if the underlying vibrator is currently available, false otherwise. */ public boolean isAvailable() { - synchronized (mLock) { - return mNativeWrapper.isAvailable(); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#isAvailable"); + try { + synchronized (mLock) { + return mNativeWrapper.isAvailable(); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -204,12 +215,17 @@ final class VibratorController { * <p>This will affect the state of {@link #isUnderExternalControl()}. */ public void setExternalControl(boolean externalControl) { - if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) { - return; - } - synchronized (mLock) { - mIsUnderExternalControl = externalControl; - mNativeWrapper.setExternalControl(externalControl); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setExternalControl(" + externalControl + ")"); + try { + if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) { + return; + } + synchronized (mLock) { + mIsUnderExternalControl = externalControl; + mNativeWrapper.setExternalControl(externalControl); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -218,28 +234,38 @@ final class VibratorController { * if given {@code effect} is {@code null}. */ public void updateAlwaysOn(int id, @Nullable PrebakedSegment prebaked) { - if (!mVibratorInfo.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) { - return; - } - synchronized (mLock) { - if (prebaked == null) { - mNativeWrapper.alwaysOnDisable(id); - } else { - mNativeWrapper.alwaysOnEnable(id, prebaked.getEffectId(), - prebaked.getEffectStrength()); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn"); + try { + if (!mVibratorInfo.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) { + return; + } + synchronized (mLock) { + if (prebaked == null) { + mNativeWrapper.alwaysOnDisable(id); + } else { + mNativeWrapper.alwaysOnEnable(id, prebaked.getEffectId(), + prebaked.getEffectStrength()); + } } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } /** Set the vibration amplitude. This will NOT affect the state of {@link #isVibrating()}. */ public void setAmplitude(float amplitude) { - synchronized (mLock) { - if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) { - mNativeWrapper.setAmplitude(amplitude); - } - if (mIsVibrating) { - mCurrentAmplitude = amplitude; + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude"); + try { + synchronized (mLock) { + if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) { + mNativeWrapper.setAmplitude(amplitude); + } + if (mIsVibrating) { + mCurrentAmplitude = amplitude; + } } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -253,13 +279,18 @@ final class VibratorController { * do not support the input or a negative number if the operation failed. */ public long on(long milliseconds, long vibrationId) { - synchronized (mLock) { - long duration = mNativeWrapper.on(milliseconds, vibrationId); - if (duration > 0) { - mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on"); + try { + synchronized (mLock) { + long duration = mNativeWrapper.on(milliseconds, vibrationId); + if (duration > 0) { + mCurrentAmplitude = -1; + notifyListenerOnVibrating(true); + } + return duration; } - return duration; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -273,6 +304,7 @@ final class VibratorController { * do not support the input or a negative number if the operation failed. */ public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)"); synchronized (mLock) { Parcel vendorData = Parcel.obtain(); try { @@ -288,6 +320,7 @@ final class VibratorController { return duration; } finally { vendorData.recycle(); + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } } @@ -302,14 +335,19 @@ final class VibratorController { * do not support the input or a negative number if the operation failed. */ public long on(PrebakedSegment prebaked, long vibrationId) { - synchronized (mLock) { - long duration = mNativeWrapper.perform(prebaked.getEffectId(), - prebaked.getEffectStrength(), vibrationId); - if (duration > 0) { - mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)"); + try { + synchronized (mLock) { + long duration = mNativeWrapper.perform(prebaked.getEffectId(), + prebaked.getEffectStrength(), vibrationId); + if (duration > 0) { + mCurrentAmplitude = -1; + notifyListenerOnVibrating(true); + } + return duration; } - return duration; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -323,16 +361,21 @@ final class VibratorController { * do not support the input or a negative number if the operation failed. */ public long on(PrimitiveSegment[] primitives, long vibrationId) { - if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { - return 0; - } - synchronized (mLock) { - long duration = mNativeWrapper.compose(primitives, vibrationId); - if (duration > 0) { - mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)"); + try { + if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { + return 0; + } + synchronized (mLock) { + long duration = mNativeWrapper.compose(primitives, vibrationId); + if (duration > 0) { + mCurrentAmplitude = -1; + notifyListenerOnVibrating(true); + } + return duration; } - return duration; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -345,17 +388,22 @@ final class VibratorController { * @return The duration of the effect playing, or 0 if unsupported. */ public long on(RampSegment[] primitives, long vibrationId) { - if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) { - return 0; - } - synchronized (mLock) { - int braking = mVibratorInfo.getDefaultBraking(); - long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId); - if (duration > 0) { - mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)"); + try { + if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) { + return 0; + } + synchronized (mLock) { + int braking = mVibratorInfo.getDefaultBraking(); + long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId); + if (duration > 0) { + mCurrentAmplitude = -1; + notifyListenerOnVibrating(true); + } + return duration; } - return duration; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -365,10 +413,15 @@ final class VibratorController { * <p>This will affect the state of {@link #isVibrating()}. */ public void off() { - synchronized (mLock) { - mNativeWrapper.off(); - mCurrentAmplitude = 0; - notifyListenerOnVibrating(false); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#off"); + try { + synchronized (mLock) { + mNativeWrapper.off(); + mCurrentAmplitude = 0; + notifyListenerOnVibrating(false); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 799934af54c0..899f0b121a8d 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -462,20 +462,31 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override // Binder call public void performHapticFeedback(int uid, int deviceId, String opPkg, int constant, String reason, int flags, int privFlags) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback"); // Note that the `performHapticFeedback` method does not take a token argument from the // caller, and instead, uses this service as the token. This is to mitigate performance // impact that would otherwise be caused due to marshal latency. Haptic feedback effects are // short-lived, so we don't need to cancel when the process dies. - performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */ - this, flags, privFlags); + try { + performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */ + this, flags, privFlags); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } } @Override // Binder call public void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg, int constant, int inputDeviceId, int inputSource, String reason, int flags, int privFlags) { - performHapticFeedbackForInputDeviceInternal(uid, deviceId, opPkg, constant, inputDeviceId, - inputSource, reason, /* token= */ this, flags, privFlags); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice"); + try { + performHapticFeedbackForInputDeviceInternal(uid, deviceId, opPkg, constant, + inputDeviceId, + inputSource, reason, /* token= */ this, flags, privFlags); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } } /** @@ -919,30 +930,25 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") @Nullable private Vibration.EndInfo startVibrationOnThreadLocked(VibrationStepConductor conductor) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationThreadLocked"); - try { - HalVibration vib = conductor.getVibration(); - int mode = startAppOpModeLocked(vib.callerInfo); - switch (mode) { - case AppOpsManager.MODE_ALLOWED: - Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); - // Make sure mCurrentVibration is set while triggering the VibrationThread. - mCurrentVibration = conductor; - if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) { - // Shouldn't happen. The method call already logs a wtf. - mCurrentVibration = null; // Aborted. - return new Vibration.EndInfo(Status.IGNORED_ERROR_SCHEDULING); - } - return null; - case AppOpsManager.MODE_ERRORED: - Slog.w(TAG, "Start AppOpsManager operation errored for uid " - + vib.callerInfo.uid); - return new Vibration.EndInfo(Status.IGNORED_ERROR_APP_OPS); - default: - return new Vibration.EndInfo(Status.IGNORED_APP_OPS); - } - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + HalVibration vib = conductor.getVibration(); + int mode = startAppOpModeLocked(vib.callerInfo); + switch (mode) { + case AppOpsManager.MODE_ALLOWED: + Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); + // Make sure mCurrentVibration is set while triggering the VibrationThread. + mCurrentVibration = conductor; + if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) { + // Shouldn't happen. The method call already logs a wtf. + mCurrentVibration = null; // Aborted. + return new Vibration.EndInfo(Status.IGNORED_ERROR_SCHEDULING); + } + return null; + case AppOpsManager.MODE_ERRORED: + Slog.w(TAG, "Start AppOpsManager operation errored for uid " + + vib.callerInfo.uid); + return new Vibration.EndInfo(Status.IGNORED_ERROR_APP_OPS); + default: + return new Vibration.EndInfo(Status.IGNORED_APP_OPS); } } @@ -1050,21 +1056,16 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") private void reportFinishedVibrationLocked(Vibration.EndInfo vibrationEndInfo) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "reportFinishVibrationLocked"); Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); - try { - HalVibration vib = mCurrentVibration.getVibration(); - if (DEBUG) { - Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " - + vibrationEndInfo); - } - // DO NOT write metrics at this point, wait for the VibrationThread to report the - // vibration was released, after all cleanup. The metrics will be reported then. - endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false); - finishAppOpModeLocked(vib.callerInfo); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + HalVibration vib = mCurrentVibration.getVibration(); + if (DEBUG) { + Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " + + vibrationEndInfo); } + // DO NOT write metrics at this point, wait for the VibrationThread to report the + // vibration was released, after all cleanup. The metrics will be reported then. + endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false); + finishAppOpModeLocked(vib.callerInfo); } private void onSyncedVibrationComplete(long vibrationId) { @@ -1418,40 +1419,34 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") @Nullable - private SparseArray<PrebakedSegment> fixupAlwaysOnEffectsLocked( - CombinedVibration effect) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "fixupAlwaysOnEffectsLocked"); - try { - SparseArray<VibrationEffect> effects; - if (effect instanceof CombinedVibration.Mono) { - VibrationEffect syncedEffect = ((CombinedVibration.Mono) effect).getEffect(); - effects = transformAllVibratorsLocked(unused -> syncedEffect); - } else if (effect instanceof CombinedVibration.Stereo) { - effects = ((CombinedVibration.Stereo) effect).getEffects(); - } else { - // Only synced combinations can be used for always-on effects. + private SparseArray<PrebakedSegment> fixupAlwaysOnEffectsLocked(CombinedVibration effect) { + SparseArray<VibrationEffect> effects; + if (effect instanceof CombinedVibration.Mono) { + VibrationEffect syncedEffect = ((CombinedVibration.Mono) effect).getEffect(); + effects = transformAllVibratorsLocked(unused -> syncedEffect); + } else if (effect instanceof CombinedVibration.Stereo) { + effects = ((CombinedVibration.Stereo) effect).getEffects(); + } else { + // Only synced combinations can be used for always-on effects. + return null; + } + SparseArray<PrebakedSegment> result = new SparseArray<>(); + for (int i = 0; i < effects.size(); i++) { + PrebakedSegment prebaked = extractPrebakedSegment(effects.valueAt(i)); + if (prebaked == null) { + Slog.e(TAG, "Only prebaked effects supported for always-on."); return null; } - SparseArray<PrebakedSegment> result = new SparseArray<>(); - for (int i = 0; i < effects.size(); i++) { - PrebakedSegment prebaked = extractPrebakedSegment(effects.valueAt(i)); - if (prebaked == null) { - Slog.e(TAG, "Only prebaked effects supported for always-on."); - return null; - } - int vibratorId = effects.keyAt(i); - VibratorController vibrator = mVibrators.get(vibratorId); - if (vibrator != null && vibrator.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) { - result.put(vibratorId, prebaked); - } + int vibratorId = effects.keyAt(i); + VibratorController vibrator = mVibrators.get(vibratorId); + if (vibrator != null && vibrator.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) { + result.put(vibratorId, prebaked); } - if (result.size() == 0) { - return null; - } - return result; - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } + if (result.size() == 0) { + return null; + } + return result; } @Nullable @@ -1580,25 +1575,42 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) { - if ((mCapabilities & requiredCapabilities) != requiredCapabilities) { - // This sync step requires capabilities this device doesn't have, skipping sync... - return false; + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "prepareSyncedVibration"); + try { + if ((mCapabilities & requiredCapabilities) != requiredCapabilities) { + // This sync step requires capabilities this device doesn't have, skipping + // sync... + return false; + } + return mNativeWrapper.prepareSynced(vibratorIds); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } - return mNativeWrapper.prepareSynced(vibratorIds); } @Override public boolean triggerSyncedVibration(long vibrationId) { - return mNativeWrapper.triggerSynced(vibrationId); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "triggerSyncedVibration"); + try { + return mNativeWrapper.triggerSynced(vibrationId); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } } @Override public void cancelSyncedVibration() { - mNativeWrapper.cancelSynced(); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelSyncedVibration"); + try { + mNativeWrapper.cancelSynced(); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } } @Override public void noteVibratorOn(int uid, long duration) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOn"); try { if (duration <= 0) { // Tried to turn vibrator ON and got: @@ -1616,16 +1628,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mFrameworkStatsLogger.writeVibratorStateOnAsync(uid, duration); } catch (RemoteException e) { Slog.e(TAG, "Error logging VibratorStateChanged to ON", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @Override public void noteVibratorOff(int uid) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOff"); try { mBatteryStatsService.noteVibratorOff(uid); mFrameworkStatsLogger.writeVibratorStateOffAsync(uid); } catch (RemoteException e) { Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -1634,11 +1651,16 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "Vibration " + vibrationId + " finished with " + vibrationEndInfo); } - synchronized (mLock) { - if (mCurrentVibration != null - && mCurrentVibration.getVibration().id == vibrationId) { - reportFinishedVibrationLocked(vibrationEndInfo); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationCompleted"); + try { + synchronized (mLock) { + if (mCurrentVibration != null + && mCurrentVibration.getVibration().id == vibrationId) { + reportFinishedVibrationLocked(vibrationEndInfo); + } } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } @@ -1647,34 +1669,40 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "VibrationThread released after finished vibration"); } - synchronized (mLock) { - if (DEBUG) { - Slog.d(TAG, "Processing VibrationThread released callback"); - } - if (Build.IS_DEBUGGABLE && mCurrentVibration != null - && mCurrentVibration.getVibration().id != vibrationId) { - Slog.wtf(TAG, TextUtils.formatSimple( - "VibrationId mismatch on release. expected=%d, released=%d", - mCurrentVibration.getVibration().id, vibrationId)); - } - if (mCurrentVibration != null) { - // This is when we consider the current vibration complete, so report metrics. - mFrameworkStatsLogger.writeVibrationReportedAsync( - mCurrentVibration.getVibration().getStatsInfo( - /* completionUptimeMillis= */ SystemClock.uptimeMillis())); - mCurrentVibration = null; - } - if (mNextVibration != null) { - VibrationStepConductor nextConductor = mNextVibration; - mNextVibration = null; - Vibration.EndInfo vibrationEndInfo = startVibrationOnThreadLocked( - nextConductor); - if (vibrationEndInfo != null) { - // Failed to start the vibration, end it and report metrics right away. - endVibrationLocked(nextConductor.getVibration(), - vibrationEndInfo, /* shouldWriteStats= */ true); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationThreadReleased: " + vibrationId); + try { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Processing VibrationThread released callback"); + } + if (Build.IS_DEBUGGABLE && mCurrentVibration != null + && mCurrentVibration.getVibration().id != vibrationId) { + Slog.wtf(TAG, TextUtils.formatSimple( + "VibrationId mismatch on release. expected=%d, released=%d", + mCurrentVibration.getVibration().id, vibrationId)); + } + if (mCurrentVibration != null) { + // This is when we consider the current vibration complete, so report + // metrics. + mFrameworkStatsLogger.writeVibrationReportedAsync( + mCurrentVibration.getVibration().getStatsInfo( + /* completionUptimeMillis= */ SystemClock.uptimeMillis())); + mCurrentVibration = null; + } + if (mNextVibration != null) { + VibrationStepConductor nextConductor = mNextVibration; + mNextVibration = null; + Vibration.EndInfo vibrationEndInfo = startVibrationOnThreadLocked( + nextConductor); + if (vibrationEndInfo != null) { + // Failed to start the vibration, end it and report metrics right away. + endVibrationLocked(nextConductor.getVibration(), + vibrationEndInfo, /* shouldWriteStats= */ true); + } } } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } } @@ -1917,22 +1945,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") private void endExternalVibrateLocked(Vibration.EndInfo vibrationEndInfo, boolean continueExternalControl) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "endExternalVibrateLocked"); - try { - if (mCurrentExternalVibration == null) { - return; - } - mCurrentExternalVibration.unlinkToDeath(); - if (!continueExternalControl) { - setExternalControl(false, mCurrentExternalVibration.stats); - } - // The external control was turned off, end it and report metrics right away. - endVibrationLocked(mCurrentExternalVibration, vibrationEndInfo, - /* shouldWriteStats= */ true); - mCurrentExternalVibration = null; - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + if (mCurrentExternalVibration == null) { + return; + } + mCurrentExternalVibration.unlinkToDeath(); + if (!continueExternalControl) { + setExternalControl(false, mCurrentExternalVibration.stats); } + // The external control was turned off, end it and report metrics right away. + endVibrationLocked(mCurrentExternalVibration, vibrationEndInfo, + /* shouldWriteStats= */ true); + mCurrentExternalVibration = null; } private HapticFeedbackVibrationProvider getHapticVibrationProvider() { @@ -1987,143 +2010,160 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) { - // Create Vibration.Stats as close to the received request as possible, for tracking. - ExternalVibrationSession externalVibration = new ExternalVibrationSession(vib); - // Mute the request until we run all the checks and accept the vibration. - externalVibration.muteScale(); - boolean alreadyUnderExternalControl = false; - boolean waitForCompletion = false; + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStart"); + try { + // Create Vibration.Stats as close to the received request as possible, for + // tracking. + ExternalVibrationSession externalVibration = new ExternalVibrationSession(vib); + // Mute the request until we run all the checks and accept the vibration. + externalVibration.muteScale(); + boolean alreadyUnderExternalControl = false; + boolean waitForCompletion = false; - synchronized (mLock) { - if (!hasExternalControlCapability()) { - endVibrationLocked(externalVibration, - new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED), - /* shouldWriteStats= */ true); - return externalVibration.getScale(); - } + synchronized (mLock) { + if (!hasExternalControlCapability()) { + endVibrationLocked(externalVibration, + new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED), + /* shouldWriteStats= */ true); + return externalVibration.getScale(); + } - if (ActivityManager.checkComponentPermission(android.Manifest.permission.VIBRATE, - vib.getUid(), -1 /*owningUid*/, true /*exported*/) - != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid() - + " tried to play externally controlled vibration" - + " without VIBRATE permission, ignoring."); - endVibrationLocked(externalVibration, - new Vibration.EndInfo(Status.IGNORED_MISSING_PERMISSION), - /* shouldWriteStats= */ true); - return externalVibration.getScale(); - } + if (ActivityManager.checkComponentPermission( + android.Manifest.permission.VIBRATE, + vib.getUid(), -1 /*owningUid*/, true /*exported*/) + != PackageManager.PERMISSION_GRANTED) { + Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid() + + " tried to play externally controlled vibration" + + " without VIBRATE permission, ignoring."); + endVibrationLocked(externalVibration, + new Vibration.EndInfo(Status.IGNORED_MISSING_PERMISSION), + /* shouldWriteStats= */ true); + return externalVibration.getScale(); + } - Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked( - externalVibration.callerInfo); + Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked( + externalVibration.callerInfo); - if (vibrationEndInfo == null - && mCurrentExternalVibration != null - && mCurrentExternalVibration.isHoldingSameVibration(vib)) { - // We are already playing this external vibration, so we can return the same - // scale calculated in the previous call to this method. - return mCurrentExternalVibration.getScale(); - } + if (vibrationEndInfo == null + && mCurrentExternalVibration != null + && mCurrentExternalVibration.isHoldingSameVibration(vib)) { + // We are already playing this external vibration, so we can return the same + // scale calculated in the previous call to this method. + return mCurrentExternalVibration.getScale(); + } - if (vibrationEndInfo == null) { - // Check if ongoing vibration is more important than this vibration. - vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(externalVibration); - } + if (vibrationEndInfo == null) { + // Check if ongoing vibration is more important than this vibration. + vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(externalVibration); + } - if (vibrationEndInfo != null) { - endVibrationLocked(externalVibration, vibrationEndInfo, - /* shouldWriteStats= */ true); - return externalVibration.getScale(); - } + if (vibrationEndInfo != null) { + endVibrationLocked(externalVibration, vibrationEndInfo, + /* shouldWriteStats= */ true); + return externalVibration.getScale(); + } - if (mCurrentExternalVibration == null) { - // If we're not under external control right now, then cancel any normal - // vibration that may be playing and ready the vibrator for external control. - if (mCurrentVibration != null) { + if (mCurrentExternalVibration == null) { + // If we're not under external control right now, then cancel any normal + // vibration that may be playing and ready the vibrator for external + // control. + if (mCurrentVibration != null) { + externalVibration.stats.reportInterruptedAnotherVibration( + mCurrentVibration.getVibration().callerInfo); + clearNextVibrationLocked( + new Vibration.EndInfo(Status.IGNORED_FOR_EXTERNAL, + externalVibration.callerInfo)); + mCurrentVibration.notifyCancelled( + new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED, + externalVibration.callerInfo), + /* immediate= */ true); + waitForCompletion = true; + } + } else { + // At this point we have an externally controlled vibration playing already. + // Since the interface defines that only one externally controlled + // vibration can + // play at a time, we need to first mute the ongoing vibration and then + // return + // a scale from this function for the new one, so we can be assured that the + // ongoing will be muted in favor of the new vibration. + // + // Note that this doesn't support multiple concurrent external controls, + // as we would need to mute the old one still if it came from a different + // controller. + alreadyUnderExternalControl = true; + mCurrentExternalVibration.notifyEnded(); externalVibration.stats.reportInterruptedAnotherVibration( - mCurrentVibration.getVibration().callerInfo); - clearNextVibrationLocked( - new Vibration.EndInfo(Status.IGNORED_FOR_EXTERNAL, - externalVibration.callerInfo)); - mCurrentVibration.notifyCancelled( + mCurrentExternalVibration.callerInfo); + endExternalVibrateLocked( new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED, externalVibration.callerInfo), - /* immediate= */ true); - waitForCompletion = true; + /* continueExternalControl= */ true); } - } else { - // At this point we have an externally controlled vibration playing already. - // Since the interface defines that only one externally controlled vibration can - // play at a time, we need to first mute the ongoing vibration and then return - // a scale from this function for the new one, so we can be assured that the - // ongoing will be muted in favor of the new vibration. - // - // Note that this doesn't support multiple concurrent external controls, as we - // would need to mute the old one still if it came from a different controller. - alreadyUnderExternalControl = true; - mCurrentExternalVibration.notifyEnded(); - externalVibration.stats.reportInterruptedAnotherVibration( - mCurrentExternalVibration.callerInfo); - endExternalVibrateLocked( - new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED, - externalVibration.callerInfo), - /* continueExternalControl= */ true); - } - VibrationAttributes attrs = fixupVibrationAttributes(vib.getVibrationAttributes(), - /* effect= */ null); - if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) { - // Force update of user settings before checking if this vibration effect should - // be ignored or scaled. - mVibrationSettings.update(); - } - - mCurrentExternalVibration = externalVibration; - externalVibration.linkToDeath(this::onExternalVibrationBinderDied); - externalVibration.scale(mVibrationScaler, attrs.getUsage()); - } + VibrationAttributes attrs = fixupVibrationAttributes( + vib.getVibrationAttributes(), + /* effect= */ null); + if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) { + // Force update of user settings before checking if this vibration effect + // should be ignored or scaled. + mVibrationSettings.update(); + } - if (waitForCompletion) { - if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) { - Slog.e(TAG, "Timed out waiting for vibration to cancel"); - synchronized (mLock) { - // Trigger endExternalVibrateLocked to unlink to death recipient. - endExternalVibrateLocked( - new Vibration.EndInfo(Status.IGNORED_ERROR_CANCELLING), - /* continueExternalControl= */ false); - // Mute the request, vibration will be ignored. - externalVibration.muteScale(); + mCurrentExternalVibration = externalVibration; + externalVibration.linkToDeath(this::onExternalVibrationBinderDied); + externalVibration.scale(mVibrationScaler, attrs.getUsage()); + } + + if (waitForCompletion) { + if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) { + Slog.e(TAG, "Timed out waiting for vibration to cancel"); + synchronized (mLock) { + // Trigger endExternalVibrateLocked to unlink to death recipient. + endExternalVibrateLocked( + new Vibration.EndInfo(Status.IGNORED_ERROR_CANCELLING), + /* continueExternalControl= */ false); + // Mute the request, vibration will be ignored. + externalVibration.muteScale(); + } + return externalVibration.getScale(); } - return externalVibration.getScale(); } - } - if (!alreadyUnderExternalControl) { + if (!alreadyUnderExternalControl) { + if (DEBUG) { + Slog.d(TAG, "Vibrator going under external control."); + } + setExternalControl(true, externalVibration.stats); + } if (DEBUG) { - Slog.d(TAG, "Vibrator going under external control."); + Slog.d(TAG, "Playing external vibration: " + vib); } - setExternalControl(true, externalVibration.stats); - } - if (DEBUG) { - Slog.d(TAG, "Playing external vibration: " + vib); + // Vibrator will start receiving data from external channels after this point. + // Report current time as the vibration start time, for debugging. + externalVibration.stats.reportStarted(); + return externalVibration.getScale(); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } - // Vibrator will start receiving data from external channels after this point. - // Report current time as the vibration start time, for debugging. - externalVibration.stats.reportStarted(); - return externalVibration.getScale(); } @Override public void onExternalVibrationStop(ExternalVibration vib) { - synchronized (mLock) { - if (mCurrentExternalVibration != null - && mCurrentExternalVibration.isHoldingSameVibration(vib)) { - if (DEBUG) { - Slog.d(TAG, "Stopping external vibration: " + vib); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStop"); + try { + synchronized (mLock) { + if (mCurrentExternalVibration != null + && mCurrentExternalVibration.isHoldingSameVibration(vib)) { + if (DEBUG) { + Slog.d(TAG, "Stopping external vibration: " + vib); + } + endExternalVibrateLocked( + new Vibration.EndInfo(Status.FINISHED), + /* continueExternalControl= */ false); } - endExternalVibrateLocked( - new Vibration.EndInfo(Status.FINISHED), - /* continueExternalControl= */ false); } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 1822a80c2f95..bc11bacf8200 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -603,7 +603,8 @@ class ActivityStarter { .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid, activityInfo.applicationInfo.packageName, UserHandle.getUserId(activityInfo.applicationInfo.uid), - activityInfo.requireContentUriPermissionFromCaller); + activityInfo.requireContentUriPermissionFromCaller, + /* requestHashCode */ this.hashCode()); } else { intentGrants = supervisor.mService.mUgmInternal .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid, @@ -717,6 +718,9 @@ class ActivityStarter { * @return The starter result. */ int execute() { + // Required for logging ContentOrFileUriEventReported in the finally block. + String callerActivityName = null; + ActivityRecord launchingRecord = null; try { onExecutionStarted(); @@ -737,6 +741,7 @@ class ActivityStarter { ? Binder.getCallingUid() : mRequest.realCallingUid; launchingState = mSupervisor.getActivityMetricsLogger().notifyActivityLaunching( mRequest.intent, caller, callingUid); + callerActivityName = caller != null ? caller.info.name : null; } if (mRequest.intent != null) { @@ -812,7 +817,7 @@ class ActivityStarter { final ActivityOptions originalOptions = mRequest.activityOptions != null ? mRequest.activityOptions.getOriginalOptions() : null; // Only track the launch time of activity that will be resumed. - final ActivityRecord launchingRecord = mDoResume ? mLastStartActivityRecord : null; + launchingRecord = mDoResume ? mLastStartActivityRecord : null; // If the new record is the one that started, a new activity has created. final boolean newActivityCreated = mStartActivity == launchingRecord; // Notify ActivityMetricsLogger that the activity has launched. @@ -828,6 +833,23 @@ class ActivityStarter { return getExternalResult(res); } } finally { + // Notify UriGrantsManagerService that activity launch completed. Required for logging + // the ContentOrFileUriEventReported message. + mSupervisor.mService.mUgmInternal.notifyActivityLaunchRequestCompleted( + mRequest.hashCode(), + // isSuccessfulLaunch + launchingRecord != null, + // Intent action + mRequest.intent != null ? mRequest.intent.getAction() : null, + mRequest.realCallingUid, + callerActivityName, + // Callee UID + mRequest.activityInfo != null + ? mRequest.activityInfo.applicationInfo.uid : INVALID_UID, + // Callee Activity name + mRequest.activityInfo != null ? mRequest.activityInfo.name : null, + // isStartActivityForResult + launchingRecord != null && launchingRecord.resultTo != null); onExecutionComplete(); } } diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_2/background/cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_2/background/cpus new file mode 100644 index 000000000000..8b0fab869c1d --- /dev/null +++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_2/background/cpus @@ -0,0 +1 @@ +0-1 diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_2/top-app/cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_2/top-app/cpus new file mode 100644 index 000000000000..40c7bb2f1a2a --- /dev/null +++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_2/top-app/cpus @@ -0,0 +1 @@ +0-3 diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java index 2fbe8aab73d0..3fe038ac4031 100644 --- a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java @@ -26,6 +26,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.content.Context; import android.content.res.AssetManager; +import android.util.IntArray; import android.util.Log; import android.util.SparseArray; @@ -48,6 +49,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { private static final String TAG = CpuInfoReaderTest.class.getSimpleName(); private static final String ROOT_DIR_NAME = "CpuInfoReaderTest"; private static final String VALID_CPUSET_DIR = "valid_cpuset"; + private static final String VALID_CPUSET_2_DIR = "valid_cpuset_2"; private static final String VALID_CPUSET_WITH_EMPTY_CPUS = "valid_cpuset_with_empty_cpus"; private static final String VALID_CPUFREQ_WITH_EMPTY_AFFECTED_CPUS = "valid_cpufreq_with_empty_affected_cpus"; @@ -88,54 +90,95 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { } @Test + public void testReadCpuInfoWithUpdatedCpuset() throws Exception { + CpuInfoReader cpuInfoReader = newCpuInfoReader(getCacheFile(VALID_CPUSET_DIR), + getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(VALID_PROC_STAT)); + + SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos(); + SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = + getFirstCpuInfosWithTimeInStateSnapshot(); + + compareCpuInfos("CPU infos first snapshot", expectedCpuInfos, actualCpuInfos); + + cpuInfoReader.setCpusetDir(getCacheFile(VALID_CPUSET_2_DIR)); + cpuInfoReader.setCpuFreqDir(getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_2_DIR)); + cpuInfoReader.setProcStatFile(getCacheFile(VALID_PROC_STAT_2)); + + actualCpuInfos = cpuInfoReader.readCpuInfos(); + + IntArray cpusetCategories = new IntArray(); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP); + expectedCpuInfos = getSecondCpuInfosWithTimeInStateSnapshot(cpusetCategories); + + compareCpuInfos("CPU infos second snapshot", expectedCpuInfos, actualCpuInfos); + } + + @Test + public void testReadCpuInfoWithUpdatedCpusetBeforeStopSignal() throws Exception { + CpuInfoReader cpuInfoReader = newCpuInfoReader(getCacheFile(VALID_CPUSET_DIR), + getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(VALID_PROC_STAT)); + + SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos(); + SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = + getFirstCpuInfosWithTimeInStateSnapshot(); + + compareCpuInfos("CPU infos first snapshot", expectedCpuInfos, actualCpuInfos); + + cpuInfoReader.setCpusetDir(getCacheFile(VALID_CPUSET_2_DIR)); + // When stopping the periodic cpuset reading, the reader will create a new snapshot. + cpuInfoReader.stopPeriodicCpusetReading(); + // Any cpuset update after the stop signal should be ignored by the reader. + cpuInfoReader.setCpusetDir(getCacheFile(VALID_CPUSET_DIR)); + cpuInfoReader.setCpuFreqDir(getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_2_DIR)); + cpuInfoReader.setProcStatFile(getCacheFile(VALID_PROC_STAT_2)); + + actualCpuInfos = cpuInfoReader.readCpuInfos(); + + IntArray cpusetCategories = new IntArray(); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP); + expectedCpuInfos = getSecondCpuInfosWithTimeInStateSnapshot(cpusetCategories); + + compareCpuInfos("CPU infos second snapshot", expectedCpuInfos, actualCpuInfos); + } + + + @Test + public void testReadCpuInfoWithUpdatedCpusetAfterStopSignal() throws Exception { + CpuInfoReader cpuInfoReader = newCpuInfoReader(getCacheFile(VALID_CPUSET_DIR), + getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(VALID_PROC_STAT)); + + SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos(); + SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = + getFirstCpuInfosWithTimeInStateSnapshot(); + + compareCpuInfos("CPU infos first snapshot", expectedCpuInfos, actualCpuInfos); + + cpuInfoReader.stopPeriodicCpusetReading(); + // Any cpuset update after the stop signal should be ignored by the reader. + cpuInfoReader.setCpusetDir(getCacheFile(VALID_CPUSET_2_DIR)); + cpuInfoReader.setCpuFreqDir(getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_2_DIR)); + cpuInfoReader.setProcStatFile(getCacheFile(VALID_PROC_STAT_2)); + + actualCpuInfos = cpuInfoReader.readCpuInfos(); + + expectedCpuInfos = getSecondCpuInfosWithTimeInStateSnapshot(); + compareCpuInfos("CPU infos second snapshot", expectedCpuInfos, actualCpuInfos); + } + + @Test public void testReadCpuInfoWithTimeInState() throws Exception { CpuInfoReader cpuInfoReader = newCpuInfoReader(getCacheFile(VALID_CPUSET_DIR), getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(VALID_PROC_STAT)); SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos(); - SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = new SparseArray<>(); - expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, - FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, - /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 488_095, - /* normalizedAvailableCpuFreqKHz= */ 2_402_267, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, - /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, - /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, - /* irqTimeMillis= */ 8_146_740, /* softirqTimeMillis= */ 428_970, - /* stealTimeMillis= */ 81_950, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, - FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, - /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380, - /* normalizedAvailableCpuFreqKHz= */ 2_693_525, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, - /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, - /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, - /* irqTimeMillis= */ 14_786_940, /* softirqTimeMillis= */ 1_498_130, - /* stealTimeMillis= */ 78_780, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, - /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, - /* normalizedAvailableCpuFreqKHz= */ 1_901_608, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, - /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, - /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, - /* irqTimeMillis= */ 14_796_940, /* softirqTimeMillis= */ 1_478_130, - /* stealTimeMillis= */ 88_780, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, - /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, - /* normalizedAvailableCpuFreqKHz= */ 1_907_125, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, - /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, - /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, - /* irqTimeMillis= */ 8_136_740, /* softirqTimeMillis= */ 438_970, - /* stealTimeMillis= */ 71_950, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); + SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = + getFirstCpuInfosWithTimeInStateSnapshot(); compareCpuInfos("CPU infos first snapshot", expectedCpuInfos, actualCpuInfos); @@ -144,49 +187,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { actualCpuInfos = cpuInfoReader.readCpuInfos(); - expectedCpuInfos.clear(); - expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, - FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, - /* maxCpuFreqKHz= */ 2_600_000, /* avgTimeInStateCpuFreqKHz= */ 419_354, - /* normalizedAvailableCpuFreqKHz= */ 2_525_919, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, - /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, - /* idleTimeMillis= */ 110_000_000, /* iowaitTimeMillis= */ 1_100_000, - /* irqTimeMillis= */ 1_400_000, /* softirqTimeMillis= */ 80_000, - /* stealTimeMillis= */ 21_000, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, - FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_800_000, - /* maxCpuFreqKHz= */ 2_900_000, /* avgTimeInStateCpuFreqKHz= */ 429_032, - /* normalizedAvailableCpuFreqKHz= */ 2_503_009, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 900_000, - /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, - /* idleTimeMillis= */ 1_000_000, /* iowaitTimeMillis= */ 90_000, - /* irqTimeMillis= */ 200_000, /* softirqTimeMillis= */ 100_000, - /* stealTimeMillis= */ 100_000, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, - /* maxCpuFreqKHz= */ 2_100_000, /* avgTimeInStateCpuFreqKHz= */ 403_225, - /* normalizedAvailableCpuFreqKHz= */ 1_788_209, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, - /* niceTimeMillis= */ 2_000_000, /* systemTimeMillis= */ 0, - /* idleTimeMillis= */ 10_000_000, /* iowaitTimeMillis= */ 1_000_000, - /* irqTimeMillis= */ 20_000_000, /* softirqTimeMillis= */ 1_000_000, - /* stealTimeMillis= */ 100_000, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ false, /* curCpuFreqKHz= */ MISSING_FREQUENCY, - /* maxCpuFreqKHz= */ 2_100_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, - /* normalizedAvailableCpuFreqKHz= */ MISSING_FREQUENCY, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 2_000_000, - /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 1_000_000, - /* idleTimeMillis= */ 100_000, /* iowaitTimeMillis= */ 100_000, - /* irqTimeMillis= */ 100_000, /* softirqTimeMillis= */ 1_000_000, - /* stealTimeMillis= */ 1_000, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); + expectedCpuInfos = getSecondCpuInfosWithTimeInStateSnapshot(); compareCpuInfos("CPU infos second snapshot", expectedCpuInfos, actualCpuInfos); } @@ -592,4 +593,108 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { } return rootDir.delete(); } + + private SparseArray<CpuInfoReader.CpuInfo> getFirstCpuInfosWithTimeInStateSnapshot() { + SparseArray<CpuInfoReader.CpuInfo> cpuInfos = new SparseArray<>(); + cpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, + /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, + /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 488_095, + /* normalizedAvailableCpuFreqKHz= */ 2_402_267, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, + /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, + /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, + /* irqTimeMillis= */ 8_146_740, /* softirqTimeMillis= */ 428_970, + /* stealTimeMillis= */ 81_950, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + cpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, + /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, + /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380, + /* normalizedAvailableCpuFreqKHz= */ 2_693_525, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, + /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, + /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, + /* irqTimeMillis= */ 14_786_940, /* softirqTimeMillis= */ 1_498_130, + /* stealTimeMillis= */ 78_780, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + cpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2, + FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, + /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, + /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, + /* normalizedAvailableCpuFreqKHz= */ 1_901_608, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, + /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, + /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, + /* irqTimeMillis= */ 14_796_940, /* softirqTimeMillis= */ 1_478_130, + /* stealTimeMillis= */ 88_780, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + cpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3, + FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, + /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, + /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, + /* normalizedAvailableCpuFreqKHz= */ 1_907_125, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, + /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, + /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, + /* irqTimeMillis= */ 8_136_740, /* softirqTimeMillis= */ 438_970, + /* stealTimeMillis= */ 71_950, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + return cpuInfos; + } + + private SparseArray<CpuInfoReader.CpuInfo> getSecondCpuInfosWithTimeInStateSnapshot() { + IntArray cpusetCategories = new IntArray(); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND); + cpusetCategories.add(FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND); + return getSecondCpuInfosWithTimeInStateSnapshot(cpusetCategories); + } + + private SparseArray<CpuInfoReader.CpuInfo> getSecondCpuInfosWithTimeInStateSnapshot( + IntArray cpusetCategories) { + SparseArray<CpuInfoReader.CpuInfo> cpuInfos = new SparseArray<>(); + cpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, cpusetCategories.get(0), + /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, + /* maxCpuFreqKHz= */ 2_600_000, /* avgTimeInStateCpuFreqKHz= */ 419_354, + /* normalizedAvailableCpuFreqKHz= */ 2_525_919, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, + /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, + /* idleTimeMillis= */ 110_000_000, /* iowaitTimeMillis= */ 1_100_000, + /* irqTimeMillis= */ 1_400_000, /* softirqTimeMillis= */ 80_000, + /* stealTimeMillis= */ 21_000, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + cpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, cpusetCategories.get(1), + /* isOnline= */ true, /* curCpuFreqKHz= */ 2_800_000, + /* maxCpuFreqKHz= */ 2_900_000, /* avgTimeInStateCpuFreqKHz= */ 429_032, + /* normalizedAvailableCpuFreqKHz= */ 2_503_009, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 900_000, + /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, + /* idleTimeMillis= */ 1_000_000, /* iowaitTimeMillis= */ 90_000, + /* irqTimeMillis= */ 200_000, /* softirqTimeMillis= */ 100_000, + /* stealTimeMillis= */ 100_000, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + cpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2, cpusetCategories.get(2), + /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, + /* maxCpuFreqKHz= */ 2_100_000, /* avgTimeInStateCpuFreqKHz= */ 403_225, + /* normalizedAvailableCpuFreqKHz= */ 1_788_209, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, + /* niceTimeMillis= */ 2_000_000, /* systemTimeMillis= */ 0, + /* idleTimeMillis= */ 10_000_000, /* iowaitTimeMillis= */ 1_000_000, + /* irqTimeMillis= */ 20_000_000, /* softirqTimeMillis= */ 1_000_000, + /* stealTimeMillis= */ 100_000, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + cpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3, cpusetCategories.get(3), + /* isOnline= */ false, + /* curCpuFreqKHz= */ MISSING_FREQUENCY, /* maxCpuFreqKHz= */ 2_100_000, + /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ MISSING_FREQUENCY, + new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 2_000_000, + /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 1_000_000, + /* idleTimeMillis= */ 100_000, /* iowaitTimeMillis= */ 100_000, + /* irqTimeMillis= */ 100_000, /* softirqTimeMillis= */ 1_000_000, + /* stealTimeMillis= */ 1_000, /* guestTimeMillis= */ 0, + /* guestNiceTimeMillis= */ 0))); + return cpuInfos; + } + } diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java index 994313f345db..d9e09d8884c7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java @@ -26,6 +26,7 @@ import static com.android.server.cpu.CpuInfoReader.CpuInfo.MISSING_FREQUENCY; import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_BACKGROUND; import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_TOP_APP; import static com.android.server.cpu.CpuMonitorService.DEFAULT_MONITORING_INTERVAL_MILLISECONDS; +import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import static com.google.common.truth.Truth.assertWithMessage; @@ -75,6 +76,7 @@ public final class CpuMonitorServiceTest { private static final long TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS = 100; private static final long TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS = 150; private static final long TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS = 300; + private static final long TEST_STOP_PERIODIC_CPUSET_READING_DELAY_MILLISECONDS = 0; private static final CpuAvailabilityMonitoringConfig TEST_MONITORING_CONFIG_ALL_CPUSET = new CpuAvailabilityMonitoringConfig.Builder(CPUSET_ALL) .addThreshold(30).addThreshold(70).build(); @@ -119,7 +121,8 @@ public final class CpuMonitorServiceTest { mService = new CpuMonitorService(mMockContext, mMockCpuInfoReader, mServiceHandlerThread, /* shouldDebugMonitor= */ true, TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS, TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS, - TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS); + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS, + TEST_STOP_PERIODIC_CPUSET_READING_DELAY_MILLISECONDS); doNothing().when(() -> ServiceManager.addService(eq("cpu_monitor"), any(Binder.class), anyBoolean(), anyInt())); @@ -535,6 +538,18 @@ public final class CpuMonitorServiceTest { } @Test + public void testBootCompleted() throws Exception { + mService.onBootPhase(PHASE_BOOT_COMPLETED); + + // Message to stop periodic cpuset reading is posted on the service handler thread. Sync + // with this thread before proceeding. + syncWithHandler(mServiceHandler, /* delayMillis= */ 0); + + verify(mMockCpuInfoReader, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .stopPeriodicCpusetReading(); + } + + @Test public void testHeavyCpuLoadMonitoring() throws Exception { // TODO(b/267500110): Once heavy CPU load detection logic is added, add unittest. } @@ -567,7 +582,8 @@ public final class CpuMonitorServiceTest { mServiceHandlerThread, /* shouldDebugMonitor= */ false, TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS, TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS, - TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS); + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS, + TEST_STOP_PERIODIC_CPUSET_READING_DELAY_MILLISECONDS); startService(); } |