diff options
| author | 2024-12-07 03:30:29 +0000 | |
|---|---|---|
| committer | 2024-12-11 07:41:49 +0000 | |
| commit | afa31d32660f8c6fbdf5d60d2372196637bb99bc (patch) | |
| tree | 9f3592ce8afed285406896c134bfebe9a4495d32 | |
| parent | 707783874e51f50a8beea1d4d686f6991306a394 (diff) | |
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
17 files changed, 866 insertions, 195 deletions
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<SurfaceControl.Transaction>, + @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<DesktopActivityOrientationChangeHandler> 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<DesktopActivityOrientationChangeHandler> 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<WindowDecorLin @VisibleForTesting static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L; - private final Handler mHandler; + private final @ShellMainThread Handler mHandler; + private final @ShellMainThread ShellExecutor mMainExecutor; + private final @ShellMainThread MainCoroutineDispatcher mMainDispatcher; + private final @ShellBackgroundThread CoroutineScope mBgScope; private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; private final SplitScreenController mSplitScreenController; private final WindowManagerWrapper mWindowManagerWrapper; + private final @NonNull WindowDecorTaskResourceLoader mTaskResourceLoader; private WindowDecorationViewHolder mWindowDecorViewHolder; private View.OnClickListener mOnCaptionButtonClickListener; @@ -175,10 +177,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private OpenByDefaultDialog mOpenByDefaultDialog; private ResizeVeil mResizeVeil; - private Bitmap mAppIconBitmap; - private Bitmap mResizeVeilBitmap; - private CharSequence mAppName; private CapturedLink mCapturedLink; private Uri mGenericLink; private Uri mWebUri; @@ -205,16 +204,23 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopUserRepositories mDesktopUserRepositories; + private Runnable mLoadAppInfoRunnable; + private Runnable mSetAppInfoRunnable; + public DesktopModeWindowDecoration( Context context, @NonNull Context userContext, DisplayController displayController, + @NonNull WindowDecorTaskResourceLoader taskResourceLoader, SplitScreenController splitScreenController, DesktopUserRepositories desktopUserRepositories, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Handler handler, + @ShellMainThread Handler handler, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread MainCoroutineDispatcher mainDispatcher, + @ShellBackgroundThread CoroutineScope bgScope, @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, @@ -226,12 +232,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin MultiInstanceHelper multiInstanceHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, DesktopModeEventLogger desktopModeEventLogger) { - this (context, userContext, displayController, splitScreenController, + this (context, userContext, displayController, taskResourceLoader, splitScreenController, desktopUserRepositories, taskOrganizer, taskInfo, taskSurface, handler, - bgExecutor, choreographer, syncQueue, appHeaderViewHolderFactory, - rootTaskDisplayAreaOrganizer, genericLinksParser, assistContentRequester, - SurfaceControl.Builder::new, SurfaceControl.Transaction::new, - WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper( + mainExecutor, mainDispatcher, bgScope, bgExecutor, choreographer, syncQueue, + appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser, + assistContentRequester, SurfaceControl.Builder::new, + SurfaceControl.Transaction::new, WindowContainerTransaction::new, + SurfaceControl::new, new WindowManagerWrapper( context.getSystemService(WindowManager.class)), new SurfaceControlViewHostFactory() {}, windowDecorViewHostSupplier, @@ -244,12 +251,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Context context, @NonNull Context userContext, DisplayController displayController, + @NonNull WindowDecorTaskResourceLoader taskResourceLoader, SplitScreenController splitScreenController, DesktopUserRepositories desktopUserRepositories, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Handler handler, + @ShellMainThread Handler handler, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread MainCoroutineDispatcher mainDispatcher, + @ShellBackgroundThread CoroutineScope bgScope, @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, @@ -275,6 +286,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin surfaceControlViewHostFactory, windowDecorViewHostSupplier, desktopModeEventLogger); mSplitScreenController = splitScreenController; mHandler = handler; + mMainExecutor = mainExecutor; + mMainDispatcher = mainDispatcher; + mBgScope = bgScope; mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; @@ -288,6 +302,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWindowManagerWrapper = windowManagerWrapper; mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mDesktopUserRepositories = desktopUserRepositories; + mTaskResourceLoader = taskResourceLoader; + mTaskResourceLoader.onWindowDecorCreated(taskInfo); } /** @@ -504,6 +520,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (oldRootView != mResult.mRootView) { disposeStatusBarInputLayer(); mWindowDecorViewHolder = createViewHolder(); + // Load these only when first creating the view. + loadTaskNameAndIconInBackground((name, icon) -> { + 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<WindowDecorLin Trace.endSection(); // DesktopModeWindowDecoration#relayout } + /** + * Loads the task's name and icon in a background thread and posts the results back in the + * main thread. + */ + private void loadTaskNameAndIconInBackground(BiConsumer<CharSequence, Bitmap> 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<WindowDecorLin ); } else if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_header) { - loadAppInfoIfNeeded(); return mAppHeaderViewHolderFactory.create( mResult.mRootView, mOnCaptionTouchListener, mOnCaptionButtonClickListener, mOnCaptionLongClickListener, mOnCaptionGenericMotionListener, - mAppName, - mAppIconBitmap, mOnMaximizeHoverListener); } throw new IllegalArgumentException("Unexpected layout resource id"); @@ -1033,7 +1081,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mTaskInfo, mTaskSurface, mDisplayController, + mTaskResourceLoader, mSurfaceControlTransactionSupplier, + mMainDispatcher, + mBgScope, new OpenByDefaultDialog.DialogLifecycleListener() { @Override public void onDialogCreated() { @@ -1044,9 +1095,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin public void onDialogDismissed() { mOpenByDefaultDialog = null; } - }, - mAppIconBitmap, - mAppName + } ); } @@ -1058,50 +1107,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize(); } - private void loadAppInfoIfNeeded() { - // TODO(b/337370277): move this to another thread. - try { - Trace.beginSection("DesktopModeWindowDecoration#loadAppInfoIfNeeded"); - if (mAppIconBitmap != null && mAppName != null) { - return; - } - if (mTaskInfo.baseIntent == null) { - Slog.e(TAG, "Base intent not found in task"); - return; - } - final PackageManager pm = mUserContext.getPackageManager(); - final ActivityInfo activityInfo = - pm.getActivityInfo(mTaskInfo.baseIntent.getComponent(), 0 /* flags */); - final IconProvider provider = new IconProvider(mContext); - final Drawable appIconDrawable = provider.getIcon(activityInfo); - final Drawable badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, - UserHandle.of(mTaskInfo.userId)); - final BaseIconFactory headerIconFactory = createIconFactory(mContext, - R.dimen.desktop_mode_caption_icon_radius); - mAppIconBitmap = headerIconFactory.createIconBitmap(badgedAppIconDrawable, - 1f /* scale */); - - final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext, - R.dimen.desktop_mode_resize_veil_icon_size); - mResizeVeilBitmap = resizeVeilIconFactory - .createScaledBitmap(appIconDrawable, MODE_DEFAULT); - - final ApplicationInfo applicationInfo = activityInfo.applicationInfo; - mAppName = pm.getApplicationLabel(applicationInfo); - } catch (PackageManager.NameNotFoundException e) { - Slog.e(TAG, "Base activity's component name cannot be found on the system", e); - } finally { - Trace.endSection(); - } - } - - private BaseIconFactory createIconFactory(Context context, int dimensions) { - final Resources resources = context.getResources(); - final int densityDpi = resources.getDisplayMetrics().densityDpi; - final int iconSize = resources.getDimensionPixelSize(dimensions); - return new BaseIconFactory(context, densityDpi, iconSize); - } - private void closeDragResizeListener() { if (mDragResizeListener == null) { return; @@ -1116,9 +1121,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private void createResizeVeilIfNeeded() { if (mResizeVeil != null) return; - loadAppInfoIfNeeded(); - mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap, - mTaskSurface, mSurfaceControlTransactionSupplier, mTaskInfo); + mResizeVeil = new ResizeVeil(mContext, mDisplayController, mTaskResourceLoader, + mMainDispatcher, mBgScope, mTaskSurface, + mSurfaceControlTransactionSupplier, mTaskInfo); } /** @@ -1318,7 +1323,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @VisibleForTesting void onAssistContentReceived(@Nullable AssistContent assistContent) { mWebUri = assistContent == null ? null : AppToWebUtils.getSessionWebUri(assistContent); - loadAppInfoIfNeeded(); updateGenericLink(); final boolean supportsMultiInstance = mMultiInstanceHelper .supportsMultiInstanceSplit(mTaskInfo.baseActivity) @@ -1331,11 +1335,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .isTaskInFullImmersiveState(mTaskInfo.taskId); final boolean isBrowserApp = isBrowserApp(); mHandleMenu = mHandleMenuFactory.create( + mMainDispatcher, + mBgScope, this, mWindowManagerWrapper, + mTaskResourceLoader, mRelayoutParams.mLayoutResId, - mAppIconBitmap, - mAppName, mSplitScreenController, canEnterDesktopModeOrShowAppHandle(mContext), supportsMultiInstance, @@ -1624,12 +1629,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Override public void close() { + if (mLoadAppInfoRunnable != null) { + mBgExecutor.removeCallbacks(mLoadAppInfoRunnable); + } + if (mSetAppInfoRunnable != null) { + mMainExecutor.removeCallbacks(mSetAppInfoRunnable); + } + mTaskResourceLoader.onWindowDecorClosed(mTaskInfo); closeDragResizeListener(); closeHandleMenu(); closeManageWindowsMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); disposeStatusBarInputLayer(); + mWindowDecorViewHolder = null; if (canEnterDesktopMode(mContext) && isEducationEnabled()) { notifyNoCaptionHandle(); } @@ -1753,12 +1766,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Context context, @NonNull Context userContext, DisplayController displayController, + @NonNull WindowDecorTaskResourceLoader appResourceProvider, SplitScreenController splitScreenController, DesktopUserRepositories desktopUserRepositories, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, - Handler handler, + @ShellMainThread Handler handler, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread MainCoroutineDispatcher mainDispatcher, + @ShellBackgroundThread CoroutineScope bgScope, @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, @@ -1775,12 +1792,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin context, userContext, displayController, + appResourceProvider, splitScreenController, desktopUserRepositories, taskOrganizer, taskInfo, taskSurface, handler, + mainExecutor, + mainDispatcher, + bgScope, bgExecutor, choreographer, syncQueue, 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 1179b0cd226c..159759ede368 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 @@ -47,15 +47,25 @@ import androidx.compose.ui.graphics.toArgb import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread 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.common.calculateMenuPosition +import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.isPinned +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Handle menu opened when the appropriate button is clicked on. @@ -66,11 +76,12 @@ import com.android.wm.shell.windowdecor.extension.isPinned * Additional Options: Miscellaneous functions including screenshot and closing task. */ class HandleMenu( + @ShellMainThread private val mainDispatcher: CoroutineDispatcher, + @ShellBackgroundThread private val bgScope: CoroutineScope, private val parentDecor: DesktopModeWindowDecoration, private val windowManagerWrapper: WindowManagerWrapper, + private val taskResourceLoader: WindowDecorTaskResourceLoader, private val layoutResId: Int, - private val appIconBitmap: Bitmap?, - private val appName: CharSequence?, private val splitScreenController: SplitScreenController, private val shouldShowWindowingPill: Boolean, private val shouldShowNewWindowButton: Boolean, @@ -103,7 +114,8 @@ class HandleMenu( @VisibleForTesting var handleMenuViewContainer: AdditionalViewContainer? = null - private var handleMenuView: HandleMenuView? = null + @VisibleForTesting + var handleMenuView: HandleMenuView? = null // Position of the handle menu used for laying out the handle view. @VisibleForTesting @@ -122,6 +134,8 @@ class HandleMenu( get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton || shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton + private var loadAppInfoJob: Job? = null + init { updateHandleMenuPillPositions(captionX, captionY) } @@ -190,7 +204,7 @@ class HandleMenu( shouldShowDesktopModeButton = shouldShowDesktopModeButton, isBrowserApp = isBrowserApp ).apply { - bind(taskInfo, appIconBitmap, appName, shouldShowMoreActionsPill) + bind(taskInfo, shouldShowMoreActionsPill) this.onToDesktopClickListener = onToDesktopClickListener this.onToFullscreenClickListener = onToFullscreenClickListener this.onToSplitScreenClickListener = onToSplitScreenClickListener @@ -204,7 +218,16 @@ class HandleMenu( this.onCloseMenuClickListener = onCloseMenuClickListener this.onOutsideTouchListener = onOutsideTouchListener } - + loadAppInfoJob = bgScope.launch { + if (!isActive) return@launch + val name = taskResourceLoader.getName(taskInfo) + val icon = taskResourceLoader.getHeaderIcon(taskInfo) + withContext(mainDispatcher) { + if (!isActive) return@withContext + handleMenuView.setAppName(name) + handleMenuView.setAppIcon(icon) + } + } val x = handleMenuPosition.x.toInt() val y = handleMenuPosition.y.toInt() handleMenuViewContainer = @@ -412,6 +435,7 @@ class HandleMenu( resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL fun close() { + loadAppInfoJob?.cancel() handleMenuView?.animateCloseMenu { handleMenuViewContainer?.releaseView() handleMenuViewContainer = null @@ -439,8 +463,10 @@ class HandleMenu( private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill) private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>( R.id.collapse_menu_button) - private val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon) - private val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name) + @VisibleForTesting + val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon) + @VisibleForTesting + val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name) // Windowing Pill. private val windowingPill = rootView.requireViewById<View>(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<SurfaceControl.Transaction>, 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<Int, AppResources>() + /** + * 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<Int>() + + @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<DesktopTilingWindowDecoration>() @@ -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<Transaction>, ) { 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<AppHeaderViewHolder.HeaderData>(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<MainCoroutineDispatcher>(), + mock<CoroutineScope>(), bgExecutor, shellInit, mockShellCommandHandler, @@ -213,7 +218,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockTaskPositionerFactory, mockFocusTransitionObserver, desktopModeEventLogger, - mock<DesktopModeUiEventLogger>() + mock<DesktopModeUiEventLogger>(), + mock<WindowDecorTaskResourceLoader>() ) 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<Function1<Boolean, Unit>> 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<SurfaceControl>() @@ -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<ShellController>() + private val mockPackageManager = mock<PackageManager>() + private val mockIconProvider = mock<IconProvider>() + private val mockHeaderIconFactory = mock<BaseIconFactory>() + private val mockVeilIconFactory = mock<BaseIconFactory>() + + private lateinit var spyContext: TestableContext + private lateinit var loader: WindowDecorTaskResourceLoader + + private val userChangeListenerCaptor = argumentCaptor<UserChangeListener>() + 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<Context>() + + userChangeListener.onUserChanged(newUser, newContext) + + assertThat(loader.currentUserContext).isEqualTo(newContext) + } + + @Test + fun testUserChange_clearsCache() { + val newUser = 5000 + val newContext = mock<Context>() + 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<Drawable>() + val badgedAppIconDrawable = mock<Drawable>() + 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, |