From afa31d32660f8c6fbdf5d60d2372196637bb99bc Mon Sep 17 00:00:00 2001 From: Jorge Gil Date: Sat, 7 Dec 2024 03:30:29 +0000 Subject: Load window decor's app name/icon in the bg thread Adds a WindowDecorTaskResourceLoader class that loads and caches the app's resources shared across window decoration UI (App Header, menus, resize veil, etc). The cached resources are coupled to the window decor's lifecycle and user changes. The loader util itself is synchronous, so callers are the ones managing the switching to the bg thread and back. Bug: 360452034 Flag: EXEMPT refactor Test: manual - window decor UI loads as intended, perfetto trace shows binder calls for ActivityInfo and Drawable loading are done in shell.bg Change-Id: If85ee04d150467471741ac163fd558b0861d555d --- .../wm/shell/apptoweb/OpenByDefaultDialog.kt | 33 ++- .../com/android/wm/shell/dagger/WMShellModule.java | 32 ++- .../DesktopModeWindowDecorViewModel.java | 28 ++- .../windowdecor/DesktopModeWindowDecoration.java | 173 +++++++++------- .../com/android/wm/shell/windowdecor/HandleMenu.kt | 81 +++++--- .../com/android/wm/shell/windowdecor/ResizeVeil.kt | 29 ++- .../common/WindowDecorTaskResourceLoader.kt | 199 ++++++++++++++++++ .../tiling/DesktopTilingDecorViewModel.kt | 12 +- .../tiling/DesktopTilingWindowDecoration.kt | 40 ++-- .../windowdecor/viewholder/AppHeaderViewHolder.kt | 18 +- .../DesktopModeWindowDecorViewModelTestsBase.kt | 13 +- .../DesktopModeWindowDecorationTests.java | 33 ++- .../android/wm/shell/windowdecor/HandleMenuTest.kt | 72 +++++-- .../android/wm/shell/windowdecor/ResizeVeilTest.kt | 56 +++++- .../common/WindowDecorTaskResourceLoaderTest.kt | 224 +++++++++++++++++++++ .../tiling/DesktopTilingDecorViewModelTest.kt | 9 + .../tiling/DesktopTilingWindowDecorationTest.kt | 9 + 17 files changed, 866 insertions(+), 195 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt index a727b54b3a3f..4cc81a9e6f8f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.apptoweb import android.app.ActivityManager.RunningTaskInfo -import android.app.TaskInfo import android.content.Context import android.content.pm.verify.domain.DomainVerificationManager import android.graphics.Bitmap @@ -36,8 +35,17 @@ import android.widget.TextView import android.window.TaskConstants import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import java.util.function.Supplier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** @@ -45,13 +53,14 @@ import java.util.function.Supplier */ internal class OpenByDefaultDialog( private val context: Context, - private val taskInfo: TaskInfo, + private val taskInfo: RunningTaskInfo, private val taskSurface: SurfaceControl, private val displayController: DisplayController, + private val taskResourceLoader: WindowDecorTaskResourceLoader, private val surfaceControlTransactionSupplier: Supplier, + @ShellMainThread private val mainDispatcher: MainCoroutineDispatcher, + @ShellBackgroundThread private val bgScope: CoroutineScope, private val listener: DialogLifecycleListener, - appIconBitmap: Bitmap?, - appName: CharSequence? ) { private lateinit var dialog: OpenByDefaultDialogView private lateinit var viewHost: SurfaceControlViewHost @@ -67,11 +76,20 @@ internal class OpenByDefaultDialog( context.getSystemService(DomainVerificationManager::class.java)!! private val packageName = taskInfo.baseActivity?.packageName!! + private var loadAppInfoJob: Job? = null init { createDialog() initializeRadioButtons() - bindAppInfo(appIconBitmap, appName) + loadAppInfoJob = bgScope.launch { + if (!isActive) return@launch + val name = taskResourceLoader.getName(taskInfo) + val icon = taskResourceLoader.getHeaderIcon(taskInfo) + withContext(mainDispatcher.immediate) { + if (!isActive) return@withContext + bindAppInfo(icon, name) + } + } } /** Creates an open by default settings dialog. */ @@ -147,14 +165,15 @@ internal class OpenByDefaultDialog( } private fun closeMenu() { + loadAppInfoJob?.cancel() dialogContainer?.releaseView() dialogContainer = null listener.onDialogDismissed() } private fun bindAppInfo( - appIconBitmap: Bitmap?, - appName: CharSequence? + appIconBitmap: Bitmap, + appName: CharSequence ) { appIconView.setImageBitmap(appIconBitmap) appNameView.text = appName diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 0cd0f4a97bbf..634ec80183ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -151,6 +151,7 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; @@ -767,6 +768,8 @@ public abstract class WMShellModule { @WMSingleton @Provides static DesktopTilingDecorViewModel provideDesktopTilingViewModel(Context context, + @ShellMainThread MainCoroutineDispatcher mainDispatcher, + @ShellBackgroundThread CoroutineScope bgScope, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, @@ -775,9 +778,12 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, ReturnToDragStartAnimator returnToDragStartAnimator, @DynamicOverride DesktopUserRepositories desktopUserRepositories, - DesktopModeEventLogger desktopModeEventLogger) { + DesktopModeEventLogger desktopModeEventLogger, + WindowDecorTaskResourceLoader windowDecorTaskResourceLoader) { return new DesktopTilingDecorViewModel( context, + mainDispatcher, + bgScope, displayController, rootTaskDisplayAreaOrganizer, syncQueue, @@ -786,7 +792,8 @@ public abstract class WMShellModule { toggleResizeDesktopTaskTransitionHandler, returnToDragStartAnimator, desktopUserRepositories, - desktopModeEventLogger + desktopModeEventLogger, + windowDecorTaskResourceLoader ); } @@ -903,6 +910,8 @@ public abstract class WMShellModule { @ShellMainThread ShellExecutor shellExecutor, @ShellMainThread Handler mainHandler, @ShellMainThread Choreographer mainChoreographer, + @ShellMainThread MainCoroutineDispatcher mainDispatcher, + @ShellBackgroundThread CoroutineScope bgScope, @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, @@ -929,13 +938,15 @@ public abstract class WMShellModule { Optional activityOrientationChangeHandler, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, - DesktopModeUiEventLogger desktopModeUiEventLogger + DesktopModeUiEventLogger desktopModeUiEventLogger, + WindowDecorTaskResourceLoader taskResourceLoader ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); } return Optional.of(new DesktopModeWindowDecorViewModel(context, shellExecutor, mainHandler, - mainChoreographer, bgExecutor, shellInit, shellCommandHandler, windowManager, + mainChoreographer, mainDispatcher, bgScope, bgExecutor, + shellInit, shellCommandHandler, windowManager, taskOrganizer, desktopUserRepositories, displayController, shellController, displayInsetsController, syncQueue, transitions, desktopTasksController, desktopImmersiveController.get(), @@ -943,7 +954,18 @@ public abstract class WMShellModule { assistContentRequester, windowDecorViewHostSupplier, multiInstanceHelper, desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, - focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger)); + focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, + taskResourceLoader)); + } + + @WMSingleton + @Provides + static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader( + @NonNull Context context, @NonNull ShellInit shellInit, + @NonNull ShellController shellController, + @NonNull ShellCommandHandler shellCommandHandler) { + return new WindowDecorTaskResourceLoader(context, shellInit, shellController, + shellCommandHandler); } @WMSingleton 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 aea4bda527b4..5a05861c3a88 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 @@ -140,6 +140,7 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.extension.InsetsStateKt; @@ -150,7 +151,9 @@ import kotlin.Pair; import kotlin.Unit; import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.ExperimentalCoroutinesApi; +import kotlinx.coroutines.MainCoroutineDispatcher; import java.io.PrintWriter; import java.util.ArrayList; @@ -177,6 +180,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final ShellController mShellController; private final Context mContext; private final @ShellMainThread Handler mMainHandler; + private final @ShellMainThread MainCoroutineDispatcher mMainDispatcher; + private final @ShellBackgroundThread CoroutineScope mBgScope; private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; @@ -241,12 +246,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final FocusTransitionObserver mFocusTransitionObserver; private final DesktopModeEventLogger mDesktopModeEventLogger; private final DesktopModeUiEventLogger mDesktopModeUiEventLogger; + private final WindowDecorTaskResourceLoader mTaskResourceLoader; public DesktopModeWindowDecorViewModel( Context context, ShellExecutor shellExecutor, @ShellMainThread Handler mainHandler, Choreographer mainChoreographer, + @ShellMainThread MainCoroutineDispatcher mainDispatcher, + @ShellBackgroundThread CoroutineScope bgScope, @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, @@ -273,12 +281,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Optional activityOrientationChangeHandler, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, - DesktopModeUiEventLogger desktopModeUiEventLogger) { + DesktopModeUiEventLogger desktopModeUiEventLogger, + WindowDecorTaskResourceLoader taskResourceLoader) { this( context, shellExecutor, mainHandler, mainChoreographer, + mainDispatcher, + bgScope, bgExecutor, shellInit, shellCommandHandler, @@ -311,7 +322,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, new TaskPositionerFactory(), focusTransitionObserver, desktopModeEventLogger, - desktopModeUiEventLogger); + desktopModeUiEventLogger, + taskResourceLoader); } @VisibleForTesting @@ -320,6 +332,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, ShellExecutor shellExecutor, @ShellMainThread Handler mainHandler, Choreographer mainChoreographer, + @ShellMainThread MainCoroutineDispatcher mainDispatcher, + @ShellBackgroundThread CoroutineScope bgScope, @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, @@ -352,11 +366,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, TaskPositionerFactory taskPositionerFactory, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, - DesktopModeUiEventLogger desktopModeUiEventLogger) { + DesktopModeUiEventLogger desktopModeUiEventLogger, + WindowDecorTaskResourceLoader taskResourceLoader) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; + mMainDispatcher = mainDispatcher; + mBgScope = bgScope; mBgExecutor = bgExecutor; mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); mTaskOrganizer = taskOrganizer; @@ -418,6 +435,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mFocusTransitionObserver = focusTransitionObserver; mDesktopModeEventLogger = desktopModeEventLogger; mDesktopModeUiEventLogger = desktopModeUiEventLogger; + mTaskResourceLoader = taskResourceLoader; shellInit.addInitCallback(this::onInit, this); } @@ -1640,12 +1658,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, : mContext, mContext.createContextAsUser(UserHandle.of(taskInfo.userId), 0 /* flags */), mDisplayController, + mTaskResourceLoader, mSplitScreenController, mDesktopUserRepositories, mTaskOrganizer, taskInfo, taskSurface, mMainHandler, + mMainExecutor, + mMainDispatcher, + mBgScope, mBgExecutor, mMainChoreographer, mSyncQueue, 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 01319fb8c713..febf5669d12d 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 @@ -28,7 +28,6 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS; -import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle; import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; @@ -49,9 +48,6 @@ import android.app.assist.AssistContent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; @@ -60,13 +56,11 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.os.Trace; import android.os.UserHandle; import android.util.Size; -import android.util.Slog; import android.view.Choreographer; import android.view.InsetsState; import android.view.MotionEvent; @@ -81,8 +75,6 @@ import android.window.TaskSnapshot; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; -import com.android.launcher3.icons.BaseIconFactory; -import com.android.launcher3.icons.IconProvider; import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; @@ -102,10 +94,12 @@ import com.android.wm.shell.desktopmode.DesktopModeUtils; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; @@ -118,7 +112,11 @@ import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.MainCoroutineDispatcher; + import java.util.List; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; @@ -134,12 +132,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration { + final AppHeaderViewHolder appHeader = asAppHeader(mWindowDecorViewHolder); + if (appHeader != null) { + appHeader.setAppName(name); + appHeader.setAppIcon(icon); + } + }); } final Point position = new Point(); @@ -542,6 +566,33 @@ public class DesktopModeWindowDecoration extends WindowDecoration onResult) { + if (mWindowDecorViewHolder == null) return; + if (asAppHeader(mWindowDecorViewHolder) == null) { + // Only needed when drawing a header. + return; + } + if (mLoadAppInfoRunnable != null) { + mBgExecutor.removeCallbacks(mLoadAppInfoRunnable); + } + if (mSetAppInfoRunnable != null) { + mMainExecutor.removeCallbacks(mSetAppInfoRunnable); + } + mLoadAppInfoRunnable = () -> { + final CharSequence name = mTaskResourceLoader.getName(mTaskInfo); + final Bitmap icon = mTaskResourceLoader.getHeaderIcon(mTaskInfo); + mSetAppInfoRunnable = () -> { + onResult.accept(name, icon); + }; + mMainExecutor.execute(mSetAppInfoRunnable); + }; + mBgExecutor.execute(mLoadAppInfoRunnable); + } + private boolean isCaptionVisible() { return mTaskInfo.isVisible && mIsCaptionVisible; } @@ -761,15 +812,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration(R.id.app_info_pill) private val collapseMenuButton = appInfoPill.requireViewById( R.id.collapse_menu_button) - private val appIconView = appInfoPill.requireViewById(R.id.application_icon) - private val appNameView = appInfoPill.requireViewById(R.id.application_name) + @VisibleForTesting + val appIconView = appInfoPill.requireViewById(R.id.application_icon) + @VisibleForTesting + val appNameView = appInfoPill.requireViewById(R.id.application_name) // Windowing Pill. private val windowingPill = rootView.requireViewById(R.id.windowing_pill) @@ -509,14 +535,12 @@ class HandleMenu( /** Binds the menu views to the new data. */ fun bind( taskInfo: RunningTaskInfo, - appIconBitmap: Bitmap?, - appName: CharSequence?, shouldShowMoreActionsPill: Boolean ) { this.taskInfo = taskInfo this.style = calculateMenuStyle(taskInfo) - bindAppInfoPill(style, appIconBitmap, appName) + bindAppInfoPill(style) if (shouldShowWindowingPill) { bindWindowingPill(style) } @@ -527,6 +551,16 @@ class HandleMenu( bindOpenInAppOrBrowserPill(style) } + /** Sets the app's name. */ + fun setAppName(name: CharSequence) { + appNameView.text = name + } + + /** Sets the app's icon. */ + fun setAppIcon(icon: Bitmap) { + appIconView.setImageBitmap(icon) + } + /** Animates the menu openInAppOrBrowserg. */ fun animateOpenMenu() { if (taskInfo.isFullscreen || taskInfo.isMultiWindow) { @@ -593,22 +627,14 @@ class HandleMenu( ) } - private fun bindAppInfoPill( - style: MenuStyle, - appIconBitmap: Bitmap?, - appName: CharSequence? - ) { + private fun bindAppInfoPill(style: MenuStyle) { appInfoPill.background.setTint(style.backgroundColor) collapseMenuButton.apply { imageTintList = ColorStateList.valueOf(style.textColor) this.taskInfo = this@HandleMenuView.taskInfo } - appIconView.setImageBitmap(appIconBitmap) - appNameView.apply { - text = appName - setTextColor(style.textColor) - } + appNameView.setTextColor(style.textColor) } private fun bindWindowingPill(style: MenuStyle) { @@ -698,11 +724,12 @@ class HandleMenu( /** A factory interface to create a [HandleMenu]. */ interface HandleMenuFactory { fun create( + @ShellMainThread mainDispatcher: MainCoroutineDispatcher, + @ShellBackgroundThread bgScope: CoroutineScope, parentDecor: DesktopModeWindowDecoration, windowManagerWrapper: WindowManagerWrapper, + taskResourceLoader: WindowDecorTaskResourceLoader, layoutResId: Int, - appIconBitmap: Bitmap?, - appName: CharSequence?, splitScreenController: SplitScreenController, shouldShowWindowingPill: Boolean, shouldShowNewWindowButton: Boolean, @@ -721,11 +748,12 @@ interface HandleMenuFactory { /** A [HandleMenuFactory] implementation that creates a [HandleMenu]. */ object DefaultHandleMenuFactory : HandleMenuFactory { override fun create( + @ShellMainThread mainDispatcher: MainCoroutineDispatcher, + @ShellBackgroundThread bgScope: CoroutineScope, parentDecor: DesktopModeWindowDecoration, windowManagerWrapper: WindowManagerWrapper, + taskResourceLoader: WindowDecorTaskResourceLoader, layoutResId: Int, - appIconBitmap: Bitmap?, - appName: CharSequence?, splitScreenController: SplitScreenController, shouldShowWindowingPill: Boolean, shouldShowNewWindowButton: Boolean, @@ -740,11 +768,12 @@ object DefaultHandleMenuFactory : HandleMenuFactory { captionY: Int, ): HandleMenu { return HandleMenu( + mainDispatcher, + bgScope, parentDecor, windowManagerWrapper, + taskResourceLoader, layoutResId, - appIconBitmap, - appName, splitScreenController, shouldShowWindowingPill, shouldShowNewWindowButton, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt index 8770d35cb85c..96839ce47725 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -20,7 +20,6 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.app.ActivityManager.RunningTaskInfo import android.content.Context -import android.graphics.Bitmap import android.graphics.Color import android.graphics.PixelFormat import android.graphics.PointF @@ -38,13 +37,23 @@ import android.window.TaskConstants import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.ui.graphics.toArgb +import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.Theme import java.util.function.Supplier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Creates and updates a veil that covers task contents on resize. @@ -52,7 +61,9 @@ import java.util.function.Supplier public class ResizeVeil @JvmOverloads constructor( private val context: Context, private val displayController: DisplayController, - private val appIcon: Bitmap, + private val taskResourceLoader: WindowDecorTaskResourceLoader, + @ShellMainThread private val mainDispatcher: CoroutineDispatcher, + @ShellBackgroundThread private val bgScope: CoroutineScope, private var parentSurface: SurfaceControl, private val surfaceControlTransactionSupplier: Supplier, private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory = @@ -65,7 +76,8 @@ public class ResizeVeil @JvmOverloads constructor( private val lightColors = dynamicLightColorScheme(context) private val darkColors = dynamicDarkColorScheme(context) - private lateinit var iconView: ImageView + @VisibleForTesting + lateinit var iconView: ImageView private var iconSize = 0 /** A container surface to host the veil background and icon child surfaces. */ @@ -77,6 +89,7 @@ public class ResizeVeil @JvmOverloads constructor( private var viewHost: SurfaceControlViewHost? = null private var display: Display? = null private var veilAnimator: ValueAnimator? = null + private var loadAppInfoJob: Job? = null /** * Whether the resize veil is currently visible. @@ -142,7 +155,6 @@ public class ResizeVeil @JvmOverloads constructor( val root = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_resize_veil, null /* root */) iconView = root.requireViewById(R.id.veil_application_icon) - iconView.setImageBitmap(appIcon) val lp = WindowManager.LayoutParams( iconSize, iconSize, @@ -156,6 +168,14 @@ public class ResizeVeil @JvmOverloads constructor( iconSurface, null /* hostInputToken */) viewHost = surfaceControlViewHostFactory.create(context, display, wwm, "ResizeVeil") viewHost?.setView(root, lp) + loadAppInfoJob = bgScope.launch { + if (!isActive) return@launch + val icon = taskResourceLoader.getVeilIcon(taskInfo) + withContext(mainDispatcher) { + if (!isActive) return@withContext + iconView.setImageBitmap(icon) + } + } Trace.endSection() } @@ -401,6 +421,7 @@ public class ResizeVeil @JvmOverloads constructor( cancelAnimation() veilAnimator = null isVisible = false + loadAppInfoJob?.cancel() viewHost?.release() viewHost = null diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt new file mode 100644 index 000000000000..d87da092cccf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt @@ -0,0 +1,199 @@ +/* + * 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.common + +import android.annotation.DimenRes +import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.os.UserHandle +import androidx.tracing.Trace +import com.android.internal.annotations.VisibleForTesting +import com.android.launcher3.icons.BaseIconFactory +import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT +import com.android.launcher3.icons.IconProvider +import com.android.wm.shell.R +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.UserChangeListener +import java.io.PrintWriter +import java.util.concurrent.ConcurrentHashMap + +/** + * A utility and cache for window decoration UI resources. + */ +class WindowDecorTaskResourceLoader( + private val context: Context, + shellInit: ShellInit, + private val shellController: ShellController, + private val shellCommandHandler: ShellCommandHandler, + private val iconProvider: IconProvider, + private val headerIconFactory: BaseIconFactory, + private val veilIconFactory: BaseIconFactory, +) { + constructor( + context: Context, + shellInit: ShellInit, + shellController: ShellController, + shellCommandHandler: ShellCommandHandler, + ) : this( + context, + shellInit, + shellController, + shellCommandHandler, + IconProvider(context), + headerIconFactory = context.createIconFactory(R.dimen.desktop_mode_caption_icon_radius), + veilIconFactory = context.createIconFactory(R.dimen.desktop_mode_resize_veil_icon_size), + ) + + /** + * A map of task -> resources to prevent unnecessary binder calls and resource loading + * when multiple window decorations need the same resources, for example, the app name or icon + * used in the header and menu. + */ + @VisibleForTesting + val taskToResourceCache = ConcurrentHashMap() + /** + * Keeps track of existing tasks with a window decoration. Useful to verify that requests to + * get resources occur within the lifecycle of a window decoration, otherwise it'd be possible + * to load a tasks resources into memory without a future signal to clean up the resource. + * See [onWindowDecorClosed]. + */ + private val existingTasks = mutableSetOf() + + @VisibleForTesting + lateinit var currentUserContext: Context + + init { + shellInit.addInitCallback(this::onInit, this) + } + + private fun onInit() { + shellCommandHandler.addDumpCallback(this::dump, this) + shellController.addUserChangeListener(object : UserChangeListener { + override fun onUserChanged(newUserId: Int, userContext: Context) { + currentUserContext = userContext + // No need to hold on to resources for tasks of another profile. + taskToResourceCache.clear() + } + }) + currentUserContext = context.createContextAsUser( + UserHandle.of(ActivityManager.getCurrentUser()), /* flags= */ 0 + ) + } + + /** Returns the user readable name for this task. */ + @ShellBackgroundThread + fun getName(taskInfo: RunningTaskInfo): CharSequence { + checkWindowDecorExists(taskInfo) + val cachedResources = taskToResourceCache[taskInfo.taskId] + if (cachedResources != null) { + return cachedResources.appName + } + val resources = loadAppResources(taskInfo) + taskToResourceCache[taskInfo.taskId] = resources + return resources.appName + } + + /** Returns the icon for use by the app header and menus for this task. */ + @ShellBackgroundThread + fun getHeaderIcon(taskInfo: RunningTaskInfo): Bitmap { + checkWindowDecorExists(taskInfo) + val cachedResources = taskToResourceCache[taskInfo.taskId] + if (cachedResources != null) { + return cachedResources.appIcon + } + val resources = loadAppResources(taskInfo) + taskToResourceCache[taskInfo.taskId] = resources + return resources.appIcon + } + + /** Returns the icon for use by the resize veil for this task. */ + @ShellBackgroundThread + fun getVeilIcon(taskInfo: RunningTaskInfo): Bitmap { + checkWindowDecorExists(taskInfo) + val cachedResources = taskToResourceCache[taskInfo.taskId] + if (cachedResources != null) { + return cachedResources.veilIcon + } + val resources = loadAppResources(taskInfo) + taskToResourceCache[taskInfo.taskId] = resources + return resources.veilIcon + } + + /** Called when a window decoration for this task is created. */ + fun onWindowDecorCreated(taskInfo: RunningTaskInfo) { + existingTasks.add(taskInfo.taskId) + } + + /** Called when a window decoration for this task is closed. */ + fun onWindowDecorClosed(taskInfo: RunningTaskInfo) { + existingTasks.remove(taskInfo.taskId) + taskToResourceCache.remove(taskInfo.taskId) + } + + private fun checkWindowDecorExists(taskInfo: RunningTaskInfo) { + check(existingTasks.contains(taskInfo.taskId)) { + "Attempt to obtain resource for non-existent decoration" + } + } + + private fun loadAppResources(taskInfo: RunningTaskInfo): AppResources { + Trace.beginSection("$TAG#loadAppResources") + val pm = currentUserContext.packageManager + val activityInfo = getActivityInfo(taskInfo, pm) + val appName = pm.getApplicationLabel(activityInfo.applicationInfo) + val appIconDrawable = iconProvider.getIcon(activityInfo) + val badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, taskInfo.userHandle()) + val appIcon = headerIconFactory.createIconBitmap(badgedAppIconDrawable, /* scale= */ 1f) + val veilIcon = veilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT) + Trace.endSection() + return AppResources(appName = appName, appIcon = appIcon, veilIcon = veilIcon) + } + + private fun getActivityInfo(taskInfo: RunningTaskInfo, pm: PackageManager): ActivityInfo { + return pm.getActivityInfo(taskInfo.component(), /* flags= */ 0) + } + + private fun RunningTaskInfo.component() = baseIntent.component!! + + private fun RunningTaskInfo.userHandle() = UserHandle.of(userId) + + data class AppResources(val appName: CharSequence, val appIcon: Bitmap, val veilIcon: Bitmap) + + private fun dump(pw: PrintWriter, prefix: String) { + val innerPrefix = "$prefix " + pw.println("${prefix}$TAG") + pw.println(innerPrefix + "appResourceCache=$taskToResourceCache") + pw.println(innerPrefix + "existingTasks=$existingTasks") + } + + companion object { + private const val TAG = "AppResourceProvider" + } +} + +/** Creates an icon factory with the provided [dimensions]. */ +fun Context.createIconFactory(@DimenRes dimensions: Int): BaseIconFactory { + val densityDpi = resources.displayMetrics.densityDpi + val iconSize = resources.getDimensionPixelSize(dimensions) + return BaseIconFactory(this, densityDpi, iconSize) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt index 9db69d5c1bc5..d72da3a08de5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt @@ -31,17 +31,23 @@ import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeEventLogger -import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainCoroutineDispatcher /** Manages tiling for each displayId/userId independently. */ class DesktopTilingDecorViewModel( private val context: Context, + @ShellMainThread private val mainDispatcher: MainCoroutineDispatcher, + @ShellBackgroundThread private val bgScope: CoroutineScope, private val displayController: DisplayController, private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, private val syncQueue: SyncTransactionQueue, @@ -51,6 +57,7 @@ class DesktopTilingDecorViewModel( private val returnToDragStartAnimator: ReturnToDragStartAnimator, private val desktopUserRepositories: DesktopUserRepositories, private val desktopModeEventLogger: DesktopModeEventLogger, + private val taskResourceLoader: WindowDecorTaskResourceLoader, ) : DisplayChangeController.OnDisplayChangingListener { @VisibleForTesting var tilingTransitionHandlerByDisplayId = SparseArray() @@ -74,8 +81,11 @@ class DesktopTilingDecorViewModel( val newHandler = DesktopTilingWindowDecoration( context, + mainDispatcher, + bgScope, syncQueue, displayController, + taskResourceLoader, displayId, rootTdaOrganizer, transitions, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index 7ceac52dd2a1..6f2323347468 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -20,11 +20,9 @@ import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.content.res.Configuration import android.content.res.Resources -import android.graphics.Bitmap import android.graphics.Rect import android.os.IBinder import android.os.UserHandle -import android.util.Slog import android.view.MotionEvent import android.view.SurfaceControl import android.view.SurfaceControl.Transaction @@ -37,8 +35,6 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import com.android.internal.annotations.VisibleForTesting import com.android.launcher3.icons.BaseIconFactory -import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT -import com.android.launcher3.icons.IconProvider import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer @@ -47,11 +43,12 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeEventLogger import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger -import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_MINIMIZE import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration @@ -60,13 +57,19 @@ import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEvent import com.android.wm.shell.windowdecor.DragResizeWindowGeometry import com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE import com.android.wm.shell.windowdecor.ResizeVeil +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.extension.isFullscreen import java.util.function.Supplier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainCoroutineDispatcher class DesktopTilingWindowDecoration( private var context: Context, + @ShellMainThread private val mainDispatcher: MainCoroutineDispatcher, + @ShellBackgroundThread private val bgScope: CoroutineScope, private val syncQueue: SyncTransactionQueue, private val displayController: DisplayController, + private val taskResourceLoader: WindowDecorTaskResourceLoader, private val displayId: Int, private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, private val transitions: Transitions, @@ -110,6 +113,9 @@ class DesktopTilingWindowDecoration( context, destinationBounds, displayController, + taskResourceLoader, + mainDispatcher, + bgScope, transactionSupplier, ) val isFirstTiledApp = leftTaskResizingHelper == null && rightTaskResizingHelper == null @@ -408,11 +414,13 @@ class DesktopTilingWindowDecoration( val context: Context, val bounds: Rect, val displayController: DisplayController, + private val taskResourceLoader: WindowDecorTaskResourceLoader, + @ShellMainThread val mainDispatcher: MainCoroutineDispatcher, + @ShellBackgroundThread val bgScope: CoroutineScope, val transactionSupplier: Supplier, ) { var isInitialised = false var newBounds = Rect(bounds) - private lateinit var resizeVeilBitmap: Bitmap private lateinit var resizeVeil: ResizeVeil private val displayContext = displayController.getDisplayContext(taskInfo.displayId) private val userContext = @@ -426,26 +434,14 @@ class DesktopTilingWindowDecoration( } private fun initVeil() { - val baseActivity = taskInfo.baseActivity - if (baseActivity == null) { - Slog.e(TAG, "Base activity component not found in task") - return - } - val resizeVeilIconFactory = - displayContext?.let { - createIconFactory(displayContext, R.dimen.desktop_mode_resize_veil_icon_size) - } ?: return - val pm = userContext.getPackageManager() - val activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */) - val provider = IconProvider(displayContext) - val appIconDrawable = provider.getIcon(activityInfo) - resizeVeilBitmap = - resizeVeilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT) + displayContext ?: return resizeVeil = ResizeVeil( context = displayContext, displayController = displayController, - appIcon = resizeVeilBitmap, + taskResourceLoader = taskResourceLoader, + mainDispatcher = mainDispatcher, + bgScope = bgScope, parentSurface = desktopModeWindowDecoration.getLeash(), surfaceControlTransactionSupplier = transactionSupplier, taskInfo = taskInfo, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index f3a8b206867d..dc4fa3788778 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -72,8 +72,6 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, private val onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, - appName: CharSequence, - appIconBitmap: Bitmap, onMaximizeHoverAnimationFinishedListener: () -> Unit ) : WindowDecorationViewHolder(rootView) { @@ -154,8 +152,6 @@ class AppHeaderViewHolder( closeWindowButton.setOnTouchListener(onCaptionTouchListener) minimizeWindowButton.setOnClickListener(onCaptionButtonClickListener) minimizeWindowButton.setOnTouchListener(onCaptionTouchListener) - appNameTextView.text = appName - appIconImageView.setImageBitmap(appIconBitmap) maximizeButtonView.onHoverAnimationFinishedListener = onMaximizeHoverAnimationFinishedListener } @@ -170,6 +166,16 @@ class AppHeaderViewHolder( ) } + /** Sets the app's name in the header. */ + fun setAppName(name: CharSequence) { + appNameTextView.text = name + } + + /** Sets the app's icon in the header. */ + fun setAppIcon(icon: Bitmap) { + appIconImageView.setImageBitmap(icon) + } + private fun bindData( taskInfo: RunningTaskInfo, isTaskMaximized: Boolean, @@ -628,8 +634,6 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, - appName: CharSequence, - appIconBitmap: Bitmap, onMaximizeHoverAnimationFinishedListener: () -> Unit, ): AppHeaderViewHolder = AppHeaderViewHolder( rootView, @@ -637,8 +641,6 @@ class AppHeaderViewHolder( onCaptionButtonClickListener, onLongClickListener, onCaptionGenericMotionListener, - appName, - appIconBitmap, onMaximizeHoverAnimationFinishedListener, ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 7a37c5eec604..b5e8cebc1277 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -74,6 +74,7 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.StubTransaction import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder @@ -91,6 +92,8 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.Optional import java.util.function.Supplier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainCoroutineDispatcher /** * Utility class for tests of [DesktopModeWindowDecorViewModel] @@ -181,6 +184,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { testShellExecutor, mockMainHandler, mockMainChoreographer, + mock(), + mock(), bgExecutor, shellInit, mockShellCommandHandler, @@ -213,7 +218,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockTaskPositionerFactory, mockFocusTransitionObserver, desktopModeEventLogger, - mock() + mock(), + mock() ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -293,8 +299,9 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { val decoration = Mockito.mock(DesktopModeWindowDecoration::class.java) whenever( mockDesktopModeWindowDecorFactory.create( - any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any(), any(), any(), any()) + any(), any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.user).thenReturn(mockUserHandle) 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 db7b1f22768f..8a1a9b5ef80b 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 @@ -114,6 +114,7 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -122,6 +123,9 @@ import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.MainCoroutineDispatcher; + import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -173,6 +177,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private Choreographer mMockChoreographer; @Mock + private MainCoroutineDispatcher mMockMainCoroutineDispatcher; + @Mock + private CoroutineScope mMockBgCoroutineScope; + @Mock private SyncTransactionQueue mMockSyncQueue; @Mock private AppHeaderViewHolder.Factory mMockAppHeaderViewHolderFactory; @@ -224,6 +232,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopModeEventLogger mDesktopModeEventLogger; @Mock private DesktopRepository mDesktopRepository; + @Mock + private WindowDecorTaskResourceLoader mMockTaskResourceLoader; @Captor private ArgumentCaptor> mOnMaxMenuHoverChangeListener; @Captor @@ -234,6 +244,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private StaticMockitoSession mMockitoSession; private TestableContext mTestableContext; private final ShellExecutor mBgExecutor = new TestShellExecutor(); + private final ShellExecutor mMainExecutor = new TestShellExecutor(); private final AssistContent mAssistContent = new AssistContent(); private final Region mExclusionRegion = Region.obtain(); @@ -273,13 +284,13 @@ 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(), + when(mMockHandleMenuFactory.create(any(), any(), any(), any(), any(), anyInt(), any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt(), anyInt())) .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false); - when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(), - any())).thenReturn(mMockAppHeaderViewHolder); + when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any())) + .thenReturn(mMockAppHeaderViewHolder); when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mDesktopRepository); when(mMockDesktopUserRepositories.getProfile(anyInt())).thenReturn(mDesktopRepository); when(mMockWindowDecorViewHostSupplier.acquire(any(), eq(defaultDisplay))) @@ -1692,7 +1703,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private void verifyHandleMenuCreated(@Nullable Uri uri) { - verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(), + verify(mMockHandleMenuFactory).create(any(), any(), any(), any(), any(), anyInt(), any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)), @@ -1760,12 +1771,14 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { MaximizeMenuFactory maximizeMenuFactory, boolean relayout) { final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, - mContext, mMockDisplayController, mMockSplitScreenController, - mMockDesktopUserRepositories, mMockShellTaskOrganizer, taskInfo, - mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer, mMockSyncQueue, - mMockAppHeaderViewHolderFactory, mMockRootTaskDisplayAreaOrganizer, - mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new, - mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, + mContext, mMockDisplayController, mMockTaskResourceLoader, + mMockSplitScreenController, mMockDesktopUserRepositories, mMockShellTaskOrganizer, + taskInfo, mMockSurfaceControl, mMockHandler, mMainExecutor, + mMockMainCoroutineDispatcher, mMockBgCoroutineScope, mBgExecutor, + mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory, + mMockRootTaskDisplayAreaOrganizer, mMockGenericLinksParser, + mMockAssistContentRequester, SurfaceControl.Builder::new, mMockTransactionSupplier, + WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory, mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory, mMockMultiInstanceHelper, mMockCaptionHandleRepository, mDesktopModeEventLogger); 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 3bcbcbdd9105..cbfb57edc72d 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 @@ -24,6 +24,7 @@ import android.graphics.Bitmap import android.graphics.Color import android.graphics.Point import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner @@ -49,6 +50,13 @@ import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UND import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -68,6 +76,7 @@ import org.mockito.kotlin.whenever * Build/Install/Run: * atest WMShellUnitTests:HandleMenuTest */ +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner::class) @@ -81,14 +90,6 @@ class HandleMenuTest : ShellTestCase() { @Mock private lateinit var mockWindowManager: WindowManager @Mock - private lateinit var onClickListener: View.OnClickListener - @Mock - private lateinit var onTouchListener: View.OnTouchListener - @Mock - private lateinit var appIcon: Bitmap - @Mock - private lateinit var appName: CharSequence - @Mock private lateinit var displayController: DisplayController @Mock private lateinit var splitScreenController: SplitScreenController @@ -96,6 +97,10 @@ class HandleMenuTest : ShellTestCase() { private lateinit var displayLayout: DisplayLayout @Mock private lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost + @Mock + private lateinit var mockTaskResourceLoader: WindowDecorTaskResourceLoader + @Mock + private lateinit var mockAppIcon: Bitmap private lateinit var handleMenu: HandleMenu @@ -136,7 +141,7 @@ class HandleMenuTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) - fun testFullscreenMenuUsesSystemViewContainer() { + fun testFullscreenMenuUsesSystemViewContainer() = runTest { createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) val handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) @@ -148,7 +153,7 @@ class HandleMenuTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) - fun testFreeformMenu_usesViewHostViewContainer() { + fun testFreeformMenu_usesViewHostViewContainer() = runTest { createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer) @@ -159,7 +164,7 @@ class HandleMenuTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) - fun testSplitLeftMenu_usesSystemViewContainer() { + fun testSplitLeftMenu_usesSystemViewContainer() = runTest { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) handleMenu = createAndShowHandleMenu(SPLIT_POSITION_TOP_OR_LEFT) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) @@ -174,7 +179,7 @@ class HandleMenuTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) - fun testSplitRightMenu_usesSystemViewContainer() { + fun testSplitRightMenu_usesSystemViewContainer() = runTest { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) handleMenu = createAndShowHandleMenu(SPLIT_POSITION_BOTTOM_OR_RIGHT) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) @@ -188,7 +193,7 @@ class HandleMenuTest : ShellTestCase() { } @Test - fun testCreate_forceShowSystemBars_usesSystemViewContainer() { + fun testCreate_forceShowSystemBars_usesSystemViewContainer() = runTest { createTaskInfo(WINDOWING_MODE_FREEFORM) handleMenu = createAndShowHandleMenu(forceShowSystemBars = true) @@ -198,7 +203,7 @@ class HandleMenuTest : ShellTestCase() { } @Test - fun testCreate_forceShowSystemBars() { + fun testCreate_forceShowSystemBars() = runTest { createTaskInfo(WINDOWING_MODE_FREEFORM) handleMenu = createAndShowHandleMenu(forceShowSystemBars = true) @@ -208,6 +213,18 @@ class HandleMenuTest : ShellTestCase() { assertTrue((types and systemBars()) != 0) } + @Test + fun testCreate_loadsAppInfoInBackground() = runTest { + createTaskInfo(WINDOWING_MODE_FREEFORM) + + handleMenu = createAndShowHandleMenu() + advanceUntilIdle() + + assertThat(handleMenu.handleMenuView!!.appNameView.text).isEqualTo(APP_NAME) + val drawable = handleMenu.handleMenuView!!.appIconView.drawable as BitmapDrawable + assertThat(drawable.bitmap).isEqualTo(mockAppIcon) + } + private fun createTaskInfo(windowingMode: Int, splitPosition: Int? = null) { val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder() .setBackgroundColor(Color.YELLOW) @@ -238,9 +255,13 @@ class HandleMenuTest : ShellTestCase() { (it.arguments[1] as Rect).set(SPLIT_RIGHT_BOUNDS) } } + whenever(mockTaskResourceLoader.getName(mockDesktopWindowDecoration.mTaskInfo)) + .thenReturn(APP_NAME) + whenever(mockTaskResourceLoader.getHeaderIcon(mockDesktopWindowDecoration.mTaskInfo)) + .thenReturn(mockAppIcon) } - private fun createAndShowHandleMenu( + private fun TestScope.createAndShowHandleMenu( splitPosition: Int? = null, forceShowSystemBars: Boolean = false ): HandleMenu { @@ -262,12 +283,22 @@ class HandleMenuTest : ShellTestCase() { } else -> error("Invalid windowing mode") } - val handleMenu = HandleMenu(mockDesktopWindowDecoration, + val handleMenu = HandleMenu( + StandardTestDispatcher(testScheduler), + this, + mockDesktopWindowDecoration, WindowManagerWrapper(mockWindowManager), - layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true, - shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false, - shouldShowChangeAspectRatioButton = false, shouldShowDesktopModeButton = true, - isBrowserApp = false, null /* openInAppOrBrowserIntent */, captionWidth = HANDLE_WIDTH, + mockTaskResourceLoader, + layoutId, + splitScreenController, + shouldShowWindowingPill = true, + shouldShowNewWindowButton = true, + shouldShowManageWindowsButton = false, + shouldShowChangeAspectRatioButton = false, + shouldShowDesktopModeButton = true, + isBrowserApp = false, + null /* openInAppOrBrowserIntent */, + captionWidth = HANDLE_WIDTH, captionHeight = 50, captionX = captionX, captionY = 0, @@ -300,5 +331,6 @@ class HandleMenuTest : ShellTestCase() { private const val MENU_PILL_ELEVATION = 2 private const val MENU_PILL_SPACING_MARGIN = 4 private const val HANDLE_WIDTH = 80 + private const val APP_NAME = "Test App" } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt index e0d16aab1e07..fa3d3e4016e9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor import android.graphics.Bitmap import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display @@ -29,6 +30,13 @@ import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -54,6 +62,7 @@ import org.mockito.kotlin.whenever * Build/Install/Run: * atest WMShellUnitTests:ResizeVeilTest */ +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @@ -85,6 +94,8 @@ class ResizeVeilTest : ShellTestCase() { private lateinit var mockIconSurface: SurfaceControl @Mock private lateinit var mockTransaction: SurfaceControl.Transaction + @Mock + private lateinit var mockTaskResourceLoader: WindowDecorTaskResourceLoader private val taskInfo = TestRunningTaskInfoBuilder().build() @@ -115,7 +126,7 @@ class ResizeVeilTest : ShellTestCase() { } @Test - fun init_displayAvailable_viewHostCreated() { + fun init_displayAvailable_viewHostCreated() = runTest { createResizeVeil(withDisplayAvailable = true) verify(mockSurfaceControlViewHostFactory) @@ -123,7 +134,7 @@ class ResizeVeilTest : ShellTestCase() { } @Test - fun init_displayUnavailable_viewHostNotCreatedUntilDisplayAppears() { + fun init_displayUnavailable_viewHostNotCreatedUntilDisplayAppears() = runTest { createResizeVeil(withDisplayAvailable = false) verify(mockSurfaceControlViewHostFactory, never()) @@ -140,14 +151,14 @@ class ResizeVeilTest : ShellTestCase() { } @Test - fun dispose_removesDisplayWindowListener() { + fun dispose_removesDisplayWindowListener() = runTest { createResizeVeil().dispose() verify(mockDisplayController).removeDisplayWindowListener(any()) } @Test - fun showVeil() { + fun showVeil() = runTest { val veil = createResizeVeil() veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) @@ -159,7 +170,7 @@ class ResizeVeilTest : ShellTestCase() { } @Test - fun showVeil_displayUnavailable_doesNotShow() { + fun showVeil_displayUnavailable_doesNotShow() = runTest { val veil = createResizeVeil(withDisplayAvailable = false) veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) @@ -171,7 +182,7 @@ class ResizeVeilTest : ShellTestCase() { } @Test - fun showVeil_alreadyVisible_doesNotShowAgain() { + fun showVeil_alreadyVisible_doesNotShowAgain() = runTest { val veil = createResizeVeil() veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) @@ -184,7 +195,7 @@ class ResizeVeilTest : ShellTestCase() { } @Test - fun showVeil_reparentsVeilToNewParent() { + fun showVeil_reparentsVeilToNewParent() = runTest { val veil = createResizeVeil(parent = mock()) val newParent = mock() @@ -200,7 +211,7 @@ class ResizeVeilTest : ShellTestCase() { } @Test - fun hideVeil_alreadyHidden_doesNothing() { + fun hideVeil_alreadyHidden_doesNothing() = runTest { val veil = createResizeVeil() veil.hideVeil() @@ -208,16 +219,41 @@ class ResizeVeilTest : ShellTestCase() { verifyZeroInteractions(mockTransaction) } - private fun createResizeVeil( + @Test + fun showVeil_loadsIconInBackground() = runTest { + val veil = createResizeVeil() + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) + + advanceUntilIdle() + + verify(mockTaskResourceLoader).getVeilIcon(taskInfo) + assertThat((veil.iconView.drawable as BitmapDrawable).bitmap).isEqualTo(mockAppIcon) + } + + @Test + fun dispose_iconLoading_cancelsJob() = runTest { + val veil = createResizeVeil() + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */) + + veil.dispose() + advanceUntilIdle() + + assertThat(veil.iconView.drawable).isNull() + } + + private fun TestScope.createResizeVeil( withDisplayAvailable: Boolean = true, parent: SurfaceControl = mock() ): ResizeVeil { whenever(mockDisplayController.getDisplay(taskInfo.displayId)) .thenReturn(if (withDisplayAvailable) mockDisplay else null) + whenever(mockTaskResourceLoader.getVeilIcon(taskInfo)).thenReturn(mockAppIcon) return ResizeVeil( context, mockDisplayController, - mockAppIcon, + mockTaskResourceLoader, + StandardTestDispatcher(testScheduler), + this, parent, { mockTransaction }, mockSurfaceControlBuilderFactory, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt new file mode 100644 index 000000000000..1ec0fe794d0a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt @@ -0,0 +1,224 @@ +/* + * 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.common + +import android.app.ActivityManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.testing.TestableContext +import androidx.test.filters.SmallTest +import com.android.launcher3.icons.BaseIconFactory +import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT +import com.android.launcher3.icons.IconProvider +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.UserChangeListener +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader.AppResources +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever + +/** + * Tests for [WindowDecorTaskResourceLoader]. + * + * Build/Install/Run: atest WindowDecorTaskResourceLoaderTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class WindowDecorTaskResourceLoaderTest : ShellTestCase() { + private val testExecutor = TestShellExecutor() + private val shellInit = ShellInit(testExecutor) + private val mockShellController = mock() + private val mockPackageManager = mock() + private val mockIconProvider = mock() + private val mockHeaderIconFactory = mock() + private val mockVeilIconFactory = mock() + + private lateinit var spyContext: TestableContext + private lateinit var loader: WindowDecorTaskResourceLoader + + private val userChangeListenerCaptor = argumentCaptor() + private val userChangeListener: UserChangeListener by lazy { + userChangeListenerCaptor.firstValue + } + + @Before + fun setUp() { + spyContext = spy(mContext) + spyContext.setMockPackageManager(mockPackageManager) + doReturn(spyContext).whenever(spyContext).createContextAsUser(any(), anyInt()) + loader = + WindowDecorTaskResourceLoader( + context = spyContext, + shellInit = shellInit, + shellController = mockShellController, + shellCommandHandler = mock(), + iconProvider = mockIconProvider, + headerIconFactory = mockHeaderIconFactory, + veilIconFactory = mockVeilIconFactory, + ) + shellInit.init() + testExecutor.flushAll() + verify(mockShellController).addUserChangeListener(userChangeListenerCaptor.capture()) + } + + @Test + fun testGetName_notCached_loadsResourceAndCaches() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + + loader.getName(task) + + verify(mockPackageManager).getApplicationLabel(task.topActivityInfo!!.applicationInfo) + assertThat(loader.taskToResourceCache[task.taskId]?.appName).isNotNull() + } + + @Test + fun testGetName_cached_returnsFromCache() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock()) + + loader.getName(task) + + verifyZeroInteractions( + mockPackageManager, + mockIconProvider, + mockHeaderIconFactory, + mockVeilIconFactory, + ) + } + + @Test + fun testGetHeaderIcon_notCached_loadsResourceAndCaches() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + + loader.getHeaderIcon(task) + + verify(mockHeaderIconFactory).createIconBitmap(any(), anyFloat()) + assertThat(loader.taskToResourceCache[task.taskId]?.appIcon).isNotNull() + } + + @Test + fun testGetHeaderIcon_cached_returnsFromCache() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock()) + + loader.getHeaderIcon(task) + + verifyZeroInteractions(mockPackageManager, mockIconProvider, mockHeaderIconFactory) + } + + @Test + fun testGetVeilIcon_notCached_loadsResourceAndCaches() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + + loader.getVeilIcon(task) + + verify(mockVeilIconFactory).createScaledBitmap(any(), anyInt()) + assertThat(loader.taskToResourceCache[task.taskId]?.veilIcon).isNotNull() + } + + @Test + fun testGetVeilIcon_cached_returnsFromCache() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock()) + + loader.getVeilIcon(task) + + verifyZeroInteractions(mockPackageManager, mockIconProvider, mockVeilIconFactory) + } + + @Test + fun testUserChange_updatesContext() { + val newUser = 5000 + val newContext = mock() + + userChangeListener.onUserChanged(newUser, newContext) + + assertThat(loader.currentUserContext).isEqualTo(newContext) + } + + @Test + fun testUserChange_clearsCache() { + val newUser = 5000 + val newContext = mock() + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + loader.getName(task) + + userChangeListener.onUserChanged(newUser, newContext) + + assertThat(loader.taskToResourceCache[task.taskId]?.appName).isNull() + } + + @Test + fun testGet_nonexistentDecor_throws() { + val task = createTaskInfo(context.userId) + + assertThrows(Exception::class.java) { loader.getName(task) } + } + + private fun createTaskInfo(userId: Int): ActivityManager.RunningTaskInfo { + val appIconDrawable = mock() + val badgedAppIconDrawable = mock() + val activityInfo = ActivityInfo().apply { applicationInfo = ApplicationInfo() } + val componentName = ComponentName("com.foo", "BarActivity") + whenever(mockPackageManager.getActivityInfo(eq(componentName), anyInt())) + .thenReturn(activityInfo) + whenever(mockPackageManager.getApplicationLabel(activityInfo.applicationInfo)) + .thenReturn("Test App") + whenever(mockPackageManager.getUserBadgedIcon(appIconDrawable, UserHandle.of(userId))) + .thenReturn(badgedAppIconDrawable) + whenever(mockIconProvider.getIcon(activityInfo)).thenReturn(appIconDrawable) + whenever(mockHeaderIconFactory.createIconBitmap(badgedAppIconDrawable, 1f)) + .thenReturn(mock()) + whenever(mockVeilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)) + .thenReturn(mock()) + return TestRunningTaskInfoBuilder() + .setUserId(userId) + .setBaseIntent(Intent().apply { component = componentName }) + .build() + .apply { topActivityInfo = activityInfo } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt index 193c2c25d26d..997ece6ecadc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt @@ -32,7 +32,10 @@ import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainCoroutineDispatcher import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -47,6 +50,8 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopTilingDecorViewModelTest : ShellTestCase() { private val contextMock: Context = mock() + private val mainDispatcher: MainCoroutineDispatcher = mock() + private val bgScope: CoroutineScope = mock() private val displayControllerMock: DisplayController = mock() private val rootTdaOrganizerMock: RootTaskDisplayAreaOrganizer = mock() private val syncQueueMock: SyncTransactionQueue = mock() @@ -61,6 +66,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { private val desktopModeWindowDecorationMock: DesktopModeWindowDecoration = mock() private val desktopTilingDecoration: DesktopTilingWindowDecoration = mock() + private val taskResourceLoader: WindowDecorTaskResourceLoader = mock() private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel @Before @@ -68,6 +74,8 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { desktopTilingDecorViewModel = DesktopTilingDecorViewModel( contextMock, + mainDispatcher, + bgScope, displayControllerMock, rootTdaOrganizerMock, syncQueueMock, @@ -77,6 +85,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { returnToDragStartAnimatorMock, userRepositories, desktopModeEventLogger, + taskResourceLoader, ) whenever(contextMock.createContextAsUser(any(), any())).thenReturn(contextMock) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index 95e2151be96c..2f15c2e38855 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -45,7 +45,10 @@ import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.DragResizeWindowGeometry +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainCoroutineDispatcher import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -99,6 +102,9 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { private val desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager = mock() private val motionEvent: MotionEvent = mock() private val desktopRepository: DesktopRepository = mock() + private val mainDispatcher: MainCoroutineDispatcher = mock() + private val bgScope: CoroutineScope = mock() + private val taskResourceLoader: WindowDecorTaskResourceLoader = mock() private lateinit var tilingDecoration: DesktopTilingWindowDecoration private val split_divider_width = 10 @@ -110,8 +116,11 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { tilingDecoration = DesktopTilingWindowDecoration( context, + mainDispatcher, + bgScope, syncQueue, displayController, + taskResourceLoader, displayId, rootTdaOrganizer, transitions, -- cgit v1.2.3-59-g8ed1b