summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jorge Gil <jorgegil@google.com> 2024-12-07 03:30:29 +0000
committer Jorge Gil <jorgegil@google.com> 2024-12-11 07:41:49 +0000
commitafa31d32660f8c6fbdf5d60d2372196637bb99bc (patch)
tree9f3592ce8afed285406896c134bfebe9a4495d32
parent707783874e51f50a8beea1d4d686f6991306a394 (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
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java173
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt199
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java33
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt72
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt56
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt224
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt9
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,