Merge "[Launcher Jank] Move binder call of RotationChangeProvider to BG thread" into main
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 98de415..62cc0bb 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -93,6 +93,7 @@
*/
public void unregisterSystemUiListener() {
SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null);
+ mDesktopTaskListener = null;
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 48fc7d1..39b4f77 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -186,7 +186,7 @@
@NonNull ImageView thumbnailView,
@ColorInt int backgroundColor,
@Nullable ThumbnailData thumbnailData) {
- Bitmap bm = thumbnailData == null ? null : thumbnailData.thumbnail;
+ Bitmap bm = thumbnailData == null ? null : thumbnailData.getThumbnail();
if (thumbnailView.getVisibility() != VISIBLE) {
thumbnailView.setVisibility(VISIBLE);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 7f201b4..84874a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -37,7 +37,7 @@
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
-import com.android.app.viewcapture.SettingsAwareViewCapture;
+import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -150,7 +150,7 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnComputeInternalInsetsListener(mTaskbarInsetsComputer);
- mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+ mViewCaptureCloseable = ViewCaptureFactory.getInstance(getContext())
.startCapture(getRootView(), ".Taskbar");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index f3ac1e4..43fe251 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -394,21 +394,24 @@
* from the internal location that was used during bubble bar layout, translation values are
* calculated to position the bar at the desired location.
*
- * @param initialTranslation initial bubble bar translation at the start of drag
+ * @param initialTranslation initial bubble translation inside the bar at the start of drag
* @param location desired location of the bubble bar when drag is released
* @return point with x and y values representing translation on x and y-axis
*/
public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation,
BubbleBarLocation location) {
- // Start with bubble bar translation
- final PointF dragEndTranslation = new PointF(
- getBubbleBarDragReleaseTranslation(initialTranslation, location));
- // Apply individual bubble translation, as the order may have changed
- int viewIndex = indexOfChild(mDraggedBubbleView);
- dragEndTranslation.x += getExpandedBubbleTranslationX(viewIndex,
- getChildCount(),
- location.isOnLeft(isLayoutRtl()));
- return dragEndTranslation;
+ float dragEndTranslationX = initialTranslation.x;
+ boolean newLocationOnLeft = location.isOnLeft(isLayoutRtl());
+ if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != newLocationOnLeft) {
+ // Calculate translationX based on bar and bubble translations
+ float bubbleBarTx = getBubbleBarDragReleaseTranslation(initialTranslation, location).x;
+ float bubbleTx =
+ getExpandedBubbleTranslationX(
+ indexOfChild(mDraggedBubbleView), getChildCount(), newLocationOnLeft);
+ dragEndTranslationX = bubbleBarTx + bubbleTx;
+ }
+ // translationY does not change during drag and can be reused
+ return new PointF(dragEndTranslationX, initialTranslation.y);
}
private float getDistanceFromOtherSide() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0722e46..5f75b3b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -506,12 +506,10 @@
/**
* Notifies SystemUI to expand the selected bubble when the bubble is released.
- * @param bubbleView dragged bubble view
*/
- public void onDragRelease(@NonNull BubbleView bubbleView, BubbleBarLocation location) {
- if (bubbleView.getBubble() == null) return;
+ public void onDragRelease(BubbleBarLocation location) {
// TODO(b/330585402): send new bubble bar bounds to shell for the animation
- mSystemUiProxy.stopBubbleDrag(bubbleView.getBubble().getKey(), location);
+ mSystemUiProxy.stopBubbleDrag(location);
}
/**
@@ -556,7 +554,7 @@
* @param bubble dismissed bubble item
*/
public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
- mSystemUiProxy.removeBubble(bubble.getKey());
+ mSystemUiProxy.dragBubbleToDismiss(bubble.getKey());
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index f4b393a..604ae89 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -113,7 +113,7 @@
@Override
protected void onDragRelease() {
mBubblePinController.onDragEnd();
- mBubbleBarViewController.onDragRelease(bubbleView, mReleasedLocation);
+ mBubbleBarViewController.onDragRelease(mReleasedLocation);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index 9c3e8af..773b0b9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -30,7 +30,7 @@
import androidx.annotation.NonNull;
-import com.android.app.viewcapture.SettingsAwareViewCapture;
+import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -59,7 +59,7 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
- mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+ mViewCaptureCloseable = ViewCaptureFactory.getInstance(getContext())
.startCapture(getRootView(), ".TaskbarOverlay");
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index bffee03..e379b2a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -103,7 +103,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import com.android.app.viewcapture.SettingsAwareViewCapture;
+import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
@@ -672,7 +672,7 @@
addMultiWindowModeChangedListener(mDepthController);
initUnfoldTransitionProgressProvider();
if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
- mViewCapture = SettingsAwareViewCapture.getInstance(this).startCapture(getWindow());
+ mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow());
}
getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE);
QuickstepOnboardingPrefs.setup(this);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 535b4c2..146ff3d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -40,7 +40,7 @@
import com.android.quickstep.util.FadeOutRemoteTransition
/** A wrapper for the hidden API calls */
-class SystemApiWrapper(context: Context?) : ApiWrapper(context) {
+open class SystemApiWrapper(context: Context?) : ApiWrapper(context) {
override fun getPersons(si: ShortcutInfo) = si.persons ?: Utilities.EMPTY_PERSON_ARRAY
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 3881e9a..dc6365b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -16,14 +16,28 @@
package com.android.launcher3.uioverrides.flags
+import android.app.PendingIntent
+import android.app.blob.BlobHandle.createWithSha256
+import android.app.blob.BlobStoreManager
import android.content.Context
+import android.content.IIntentReceiver
+import android.content.IIntentSender.Stub
import android.content.Intent
+import android.content.Intent.ACTION_CREATE_DOCUMENT
+import android.content.Intent.ACTION_OPEN_DOCUMENT
import android.content.pm.PackageManager
import android.net.Uri
+import android.os.Bundle
+import android.os.IBinder
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
import android.provider.DeviceConfig
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+import android.provider.Settings.Secure
import android.text.Html
import android.util.AttributeSet
+import android.util.Base64
+import android.util.Base64.NO_PADDING
+import android.util.Base64.NO_WRAP
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import android.widget.Toast
@@ -33,11 +47,32 @@
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreference
+import com.android.launcher3.AutoInstallsLayout
import com.android.launcher3.ExtendedEditText
+import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
import com.android.launcher3.R
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.proxy.ProxyActivityStarter
import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher
+import com.android.launcher3.shortcuts.ShortcutKey
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
+import com.android.launcher3.util.LauncherLayoutBuilder
import com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT
import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_COUNT
import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN
@@ -45,12 +80,17 @@
import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
import com.android.launcher3.util.PluginManagerWrapper
+import com.android.launcher3.util.StartActivityParams
+import com.android.launcher3.util.UserIconInfo
import com.android.quickstep.util.DeviceConfigHelper
import com.android.quickstep.util.DeviceConfigHelper.Companion.NAMESPACE_LAUNCHER
import com.android.quickstep.util.DeviceConfigHelper.DebugInfo
import com.android.systemui.shared.plugins.PluginEnabler
import com.android.systemui.shared.plugins.PluginPrefs
+import java.io.OutputStreamWriter
+import java.security.MessageDigest
import java.util.Locale
+import java.util.concurrent.Executor
/** Helper class to generate UI for Device Config */
class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, attr) {
@@ -67,6 +107,9 @@
(holder.findViewById(R.id.filter_box) as TextView?)?.doAfterTextChanged {
val query: String = it.toString().lowercase(Locale.getDefault()).replace("_", " ")
filterPreferences(query, this)
+
+ // Always keep myself visible
+ this@DevOptionsUiHelper.isVisible = true
}
}
@@ -97,6 +140,7 @@
}
addIntentTargets()
addOnboardingPrefsCategory()
+ addLayoutSharePref()
}
private fun newCategory(titleText: String, subTitleText: String? = null) =
@@ -359,7 +403,7 @@
Preference(context).also {
it.title = title
it.summary = "Tap to reset"
- setOnPreferenceClickListener { _ ->
+ it.setOnPreferenceClickListener { _ ->
LauncherPrefs.getPrefs(context)
.edit()
.apply { keys.forEach { key -> remove(key) } }
@@ -370,6 +414,137 @@
}
)
+ private fun addLayoutSharePref() {
+ val model = LauncherAppState.getInstance(context).model
+ val category = newCategory("Workspace grid layout")
+ Preference(context).apply {
+ title = "Export"
+ intent =
+ createUriPickerIntent(ACTION_CREATE_DOCUMENT, MAIN_EXECUTOR) { uri ->
+ model.enqueueModelUpdateTask { _, dataModel, _ ->
+ val builder = LauncherLayoutBuilder()
+ dataModel.workspaceItems.forEach { info ->
+ val loc =
+ when (info.container) {
+ CONTAINER_DESKTOP ->
+ builder.atWorkspace(info.cellX, info.cellY, info.screenId)
+ CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId)
+ else -> return@forEach
+ }
+ loc.addItem(info)
+ }
+ dataModel.appWidgets.forEach { info ->
+ builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(info)
+ }
+
+ context.contentResolver.openOutputStream(uri).use { os ->
+ builder.build(OutputStreamWriter(os))
+ }
+
+ MAIN_EXECUTOR.execute {
+ Toast.makeText(context, "File saved", Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ category.addPreference(this)
+ }
+
+ Preference(context).apply {
+ title = "Import"
+ intent =
+ createUriPickerIntent(ACTION_OPEN_DOCUMENT, ORDERED_BG_EXECUTOR) { uri ->
+ val resolver = context.contentResolver
+ val data =
+ resolver.openInputStream(uri).use { stream ->
+ stream?.readAllBytes() ?: return@createUriPickerIntent
+ }
+
+ val digest = MessageDigest.getInstance("SHA-256").digest(data)
+ val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)
+ val blobManager = context.getSystemService(BlobStoreManager::class.java)!!
+
+ blobManager.openSession(blobManager.createSession(handle)).use { session ->
+ AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
+ session.allowPublicAccess()
+
+ session.commit(ORDERED_BG_EXECUTOR) {
+ val key = Base64.encodeToString(digest, NO_WRAP or NO_PADDING)
+ Secure.putString(resolver, LAYOUT_DIGEST_KEY, key)
+
+ MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
+ MAIN_EXECUTOR.submit { model.forceReload() }.get()
+ MODEL_EXECUTOR.submit {}.get()
+ Secure.putString(resolver, LAYOUT_DIGEST_KEY, null)
+ }
+ }
+ }
+ category.addPreference(this)
+ }
+ }
+
+ private fun LauncherLayoutBuilder.ItemTarget.addItem(info: ItemInfo) {
+ val userType: String? =
+ when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) {
+ UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK
+ UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED
+ else -> null
+ }
+ when (info.itemType) {
+ ITEM_TYPE_APPLICATION ->
+ info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) }
+ ITEM_TYPE_DEEP_SHORTCUT ->
+ ShortcutKey.fromItemInfo(info).let { key ->
+ putShortcut(key.packageName, key.id, userType)
+ }
+ ITEM_TYPE_FOLDER ->
+ (info as FolderInfo).let { folderInfo ->
+ putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder ->
+ folderInfo.getContents().forEach { folderContent ->
+ folderBuilder.addItem(folderContent)
+ }
+ }
+ }
+ ITEM_TYPE_APPWIDGET ->
+ putWidget(
+ (info as LauncherAppWidgetInfo).providerName.packageName,
+ info.providerName.className,
+ info.spanX,
+ info.spanY,
+ userType
+ )
+ }
+ }
+
+ private fun createUriPickerIntent(
+ action: String,
+ executor: Executor,
+ callback: (uri: Uri) -> Unit
+ ): Intent {
+ val pendingIntent =
+ PendingIntent(
+ object : Stub() {
+ override fun send(
+ code: Int,
+ intent: Intent,
+ resolvedType: String?,
+ allowlistToken: IBinder?,
+ finishedReceiver: IIntentReceiver?,
+ requiredPermission: String?,
+ options: Bundle?
+ ) {
+ intent.data?.let { uri -> executor.execute { callback(uri) } }
+ }
+ }
+ )
+ val params = StartActivityParams(pendingIntent, 0)
+ params.intent =
+ Intent(action)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("text/xml")
+ .putExtra(Intent.EXTRA_TITLE, "launcher_grid.xml")
+ return ProxyActivityStarter.getLaunchIntent(context, params)
+ }
+
private inner class CustomSwitchPref(
private val bindCallback: (holder: PreferenceViewHolder, pref: SwitchPreference) -> Unit
) : SwitchPreference(context) {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index e039611..da1d322 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -774,19 +774,6 @@
}
/**
- * Tells SysUI to remove the bubble with the provided key.
- * @param key the key of the bubble to show.
- */
- public void removeBubble(String key) {
- if (mBubbles == null) return;
- try {
- mBubbles.removeBubble(key);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call removeBubble");
- }
- }
-
- /**
* Tells SysUI to remove all bubbles.
*/
public void removeAllBubbles() {
@@ -828,19 +815,31 @@
/**
* Tells SysUI when the bubble stops being dragged.
* Should be called only when the bubble bar is expanded.
- * @param bubbleKey key of the bubble being dragged
* @param location location of the bubble bar
*/
- public void stopBubbleDrag(@Nullable String bubbleKey, BubbleBarLocation location) {
+ public void stopBubbleDrag(BubbleBarLocation location) {
if (mBubbles == null) return;
try {
- mBubbles.stopBubbleDrag(bubbleKey, location);
+ mBubbles.stopBubbleDrag(location);
} catch (RemoteException e) {
Log.w(TAG, "Failed call stopBubbleDrag");
}
}
/**
+ * Tells SysUI to dismiss the bubble with the provided key.
+ * @param key the key of the bubble to dismiss.
+ */
+ public void dragBubbleToDismiss(String key) {
+ if (mBubbles == null) return;
+ try {
+ mBubbles.dragBubbleToDismiss(key);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call dragBubbleToDismiss");
+ }
+ }
+
+ /**
* Tells SysUI to show user education relative to the reference point provided.
* @param position the bubble bar top center position in Screen coordinates.
*/
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index f6eef62..7ebb767 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -153,7 +153,7 @@
Preconditions.assertUIThread();
boolean lowResolution = !mHighResLoadingState.isEnabled();
- if (task.thumbnail != null && task.thumbnail.thumbnail != null
+ if (task.thumbnail != null && task.thumbnail.getThumbnail() != null
&& (!task.thumbnail.reducedResolution || lowResolution)) {
// Nothing to load, the thumbnail is already high-resolution or matches what the
// request, so just callback
@@ -189,7 +189,7 @@
Preconditions.assertUIThread();
ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
- if (cachedThumbnail != null && cachedThumbnail.thumbnail != null
+ if (cachedThumbnail != null && cachedThumbnail.getThumbnail() != null
&& (!cachedThumbnail.reducedResolution || lowResolution)) {
// Already cached, lets use that thumbnail
callback.accept(cachedThumbnail);
@@ -200,7 +200,7 @@
() -> {
ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
.getTaskThumbnail(key.id, lowResolution);
- return thumbnailData.thumbnail != null ? thumbnailData
+ return thumbnailData.getThumbnail() != null ? thumbnailData
: ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
},
MAIN_EXECUTOR,
@@ -210,7 +210,7 @@
if (enableGridOnlyOverview() && result.reducedResolution
&& getHighResLoadingState().isEnabled()) {
ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
- if (newCachedThumbnail != null && newCachedThumbnail.thumbnail != null
+ if (newCachedThumbnail != null && newCachedThumbnail.getThumbnail() != null
&& !newCachedThumbnail.reducedResolution) {
return;
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index df1879e..7e7c794 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -983,6 +983,7 @@
void onDestroy() {
SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener(
mSplitSelectListener);
+ mSplitSelectListener = null;
}
/**
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 85d4f4b..5e42b90 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -136,7 +136,7 @@
RectF startingTaskRect = new RectF();
final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
mLauncher, mLauncher.getDragLayer(),
- controller.screenshotTask(runningTaskInfo.taskId).thumbnail,
+ controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(),
null /* icon */, startingTaskRect);
RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
.getIconCache()
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index f7b6496..fed6af5 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -202,6 +202,17 @@
}
/**
+ * Sets TaskOverlayFactory without binding a task.
+ *
+ * @deprecated Should only be used when using new
+ * {@link com.android.quickstep.task.thumbnail.TaskThumbnailView}.
+ */
+ @Deprecated
+ public void setTaskOverlayFactory(TaskOverlayFactory taskOverlayFactory) {
+ mTaskOverlayFactory = taskOverlayFactory;
+ }
+
+ /**
* Updates the thumbnail.
*
* @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
@@ -215,8 +226,8 @@
boolean refreshNow) {
mTask = task;
boolean thumbnailWasNull = mThumbnailData == null;
- mThumbnailData =
- (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
+ mThumbnailData = (thumbnailData != null && thumbnailData.getThumbnail() != null)
+ ? thumbnailData : null;
if (mTask != null) {
updateSplashView(mTask.icon);
}
@@ -241,8 +252,8 @@
* @param shouldRefreshOverlay whether to re-initialize overlay
*/
private void refresh(boolean shouldRefreshOverlay) {
- if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
- Bitmap bm = mThumbnailData.thumbnail;
+ if (mThumbnailData != null && mThumbnailData.getThumbnail() != null) {
+ Bitmap bm = mThumbnailData.getThumbnail();
bm.prepareToDraw();
mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(mBitmapShader);
@@ -306,8 +317,10 @@
}
RectF bitmapRect = new RectF(
- 0, 0,
- mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
+ 0,
+ 0,
+ mThumbnailData.getThumbnail().getWidth(),
+ mThumbnailData.getThumbnail().getHeight());
RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
// The position helper matrix tells us how to transform the bitmap to fit the view, the
@@ -506,13 +519,13 @@
}
private boolean isThumbnailAspectRatioDifferentFromThumbnailData() {
- if (mThumbnailData == null || mThumbnailData.thumbnail == null) {
+ if (mThumbnailData == null || mThumbnailData.getThumbnail() == null) {
return false;
}
float thumbnailViewAspect = getWidth() / (float) getHeight();
- float thumbnailDataAspect =
- mThumbnailData.thumbnail.getWidth() / (float) mThumbnailData.thumbnail.getHeight();
+ float thumbnailDataAspect = mThumbnailData.getThumbnail().getWidth()
+ / (float) mThumbnailData.getThumbnail().getHeight();
return isRelativePercentDifferenceGreaterThan(thumbnailViewAspect,
thumbnailDataAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT);
@@ -563,8 +576,8 @@
DeviceProfile dp = mContainer.getDeviceProfile();
mPreviewPositionHelper.setOrientationChanged(false);
if (mBitmapShader != null && mThumbnailData != null) {
- mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
- mThumbnailData.thumbnail.getHeight());
+ mPreviewRect.set(0, 0, mThumbnailData.getThumbnail().getWidth(),
+ mThumbnailData.getThumbnail().getHeight());
int currentRotation = getTaskView().getOrientedState().getRecentsActivityRotation();
boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
@@ -597,7 +610,7 @@
if (mThumbnailData == null) {
return null;
}
- return mThumbnailData.thumbnail;
+ return mThumbnailData.getThumbnail();
}
/**
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index d19f09c..baeaa87 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -630,6 +630,7 @@
)
)
if (enableRefactorTaskThumbnail()) {
+ taskThumbnailViewDeprecated.setTaskOverlayFactory(taskOverlayFactory)
bindTaskThumbnailView()
} else {
taskThumbnailViewDeprecated.bind(task, taskOverlayFactory)
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index cf86528..175d6ec 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -19,6 +19,8 @@
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
+import static com.android.launcher3.util.UserIconInfo.TYPE_CLONED;
+import static com.android.launcher3.util.UserIconInfo.TYPE_WORK;
import android.content.ComponentName;
import android.content.ContentValues;
@@ -34,6 +36,7 @@
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.Process;
+import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AttributeSet;
@@ -56,6 +59,7 @@
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.Partner;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.UserIconInfo;
import com.android.launcher3.widget.LauncherWidgetHolder;
import org.xmlpull.v1.XmlPullParser;
@@ -63,6 +67,7 @@
import java.io.IOException;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Supplier;
@@ -125,34 +130,38 @@
private static final String TAG_INCLUDE = "include";
public static final String TAG_WORKSPACE = "workspace";
private static final String TAG_APP_ICON = "appicon";
- private static final String TAG_AUTO_INSTALL = "autoinstall";
- private static final String TAG_FOLDER = "folder";
- private static final String TAG_APPWIDGET = "appwidget";
+ public static final String TAG_AUTO_INSTALL = "autoinstall";
+ public static final String TAG_FOLDER = "folder";
+ public static final String TAG_APPWIDGET = "appwidget";
protected static final String TAG_SEARCH_WIDGET = "searchwidget";
- private static final String TAG_SHORTCUT = "shortcut";
+ public static final String TAG_SHORTCUT = "shortcut";
private static final String TAG_EXTRA = "extra";
- private static final String ATTR_CONTAINER = "container";
- private static final String ATTR_RANK = "rank";
+ public static final String ATTR_CONTAINER = "container";
+ public static final String ATTR_RANK = "rank";
- private static final String ATTR_PACKAGE_NAME = "packageName";
- private static final String ATTR_CLASS_NAME = "className";
- private static final String ATTR_TITLE = "title";
- private static final String ATTR_TITLE_TEXT = "titleText";
- private static final String ATTR_SCREEN = "screen";
- private static final String ATTR_SHORTCUT_ID = "shortcutId";
+ public static final String ATTR_PACKAGE_NAME = "packageName";
+ public static final String ATTR_CLASS_NAME = "className";
+ public static final String ATTR_TITLE = "title";
+ public static final String ATTR_TITLE_TEXT = "titleText";
+ public static final String ATTR_SCREEN = "screen";
+ public static final String ATTR_SHORTCUT_ID = "shortcutId";
// x and y can be specified as negative integers, in which case -1 represents the
// last row / column, -2 represents the second last, and so on.
- private static final String ATTR_X = "x";
- private static final String ATTR_Y = "y";
+ public static final String ATTR_X = "x";
+ public static final String ATTR_Y = "y";
- private static final String ATTR_SPAN_X = "spanX";
- private static final String ATTR_SPAN_Y = "spanY";
+ public static final String ATTR_SPAN_X = "spanX";
+ public static final String ATTR_SPAN_Y = "spanY";
// Attrs for "Include"
private static final String ATTR_WORKSPACE = "workspace";
+ public static final String ATTR_USER_TYPE = "userType";
+ public static final String USER_TYPE_WORK = "work";
+ public static final String USER_TYPE_CLONED = "cloned";
+
// Style attrs -- "Extra"
private static final String ATTR_KEY = "key";
private static final String ATTR_VALUE = "value";
@@ -168,6 +177,8 @@
protected final SourceResources mSourceRes;
protected final Supplier<XmlPullParser> mInitialLayoutSupplier;
+ private final Map<String, Long> mUserTypeToSerial;
+
private final InvariantDeviceProfile mIdp;
private final int mRowCount;
private final int mColumnCount;
@@ -204,15 +215,25 @@
mRowCount = mIdp.numRows;
mColumnCount = mIdp.numColumns;
mActivityOverride = ApiWrapper.INSTANCE.get(context).getActivityOverrides();
+
+ mUserTypeToSerial = new HashMap<>();
+ UserCache cache = UserCache.getInstance(context);
+ for (UserHandle user : cache.getUserProfiles()) {
+ UserIconInfo uii = cache.getUserInfo(user);
+ switch (uii.type) {
+ case TYPE_WORK -> mUserTypeToSerial.put(USER_TYPE_WORK, uii.userSerial);
+ case TYPE_CLONED -> mUserTypeToSerial.put(USER_TYPE_CLONED, uii.userSerial);
+ }
+ }
}
/**
* Loads the layout in the db and returns the number of entries added on the desktop.
*/
- public int loadLayout(SQLiteDatabase db, IntArray screenIds) {
+ public int loadLayout(SQLiteDatabase db) {
mDb = db;
try {
- return parseLayout(mInitialLayoutSupplier.get(), screenIds);
+ return parseLayout(mInitialLayoutSupplier.get());
} catch (Exception e) {
Log.e(TAG, "Error parsing layout: ", e);
return -1;
@@ -222,7 +243,7 @@
/**
* Parses the layout and returns the number of elements added on the homescreen.
*/
- protected int parseLayout(XmlPullParser parser, IntArray screenIds)
+ protected int parseLayout(XmlPullParser parser)
throws XmlPullParserException, IOException {
beginDocument(parser, mRootTag);
final int depth = parser.getDepth();
@@ -235,7 +256,7 @@
if (type != XmlPullParser.START_TAG) {
continue;
}
- count += parseAndAddNode(parser, tagParserMap, screenIds);
+ count += parseAndAddNode(parser, tagParserMap);
}
return count;
}
@@ -259,14 +280,14 @@
* Parses the current node and returns the number of elements added.
*/
protected int parseAndAddNode(
- XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)
+ XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap)
throws XmlPullParserException, IOException {
if (TAG_INCLUDE.equals(parser.getName())) {
final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
if (resId != 0) {
// recursively load some more favorites, why not?
- return parseLayout(mSourceRes.getXml(resId), screenIds);
+ return parseLayout(mSourceRes.getXml(resId));
} else {
return 0;
}
@@ -284,22 +305,17 @@
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount));
mValues.put(Favorites.CELLY,
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount));
+ Long profileId = mUserTypeToSerial.get(getAttributeValue(parser, ATTR_USER_TYPE));
+ if (profileId != null) {
+ mValues.put(Favorites.PROFILE_ID, profileId);
+ }
TagParser tagParser = tagParserMap.get(parser.getName());
if (tagParser == null) {
if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName());
return 0;
}
- int newElementId = tagParser.parseAndAdd(parser);
- if (newElementId >= 0) {
- // Keep track of the set of screens which need to be added to the db.
- if (!screenIds.contains(screenId) &&
- container == Favorites.CONTAINER_DESKTOP) {
- screenIds.add(screenId);
- }
- return 1;
- }
- return 0;
+ return tagParser.parseAndAdd(parser) >= 0 ? 1 : 0;
}
protected int addShortcut(String title, Intent intent, int type) {
@@ -311,10 +327,11 @@
mValues.put(Favorites.SPANY, 1);
mValues.put(Favorites._ID, id);
- if (type == ITEM_TYPE_APPLICATION) {
- ComponentName cn = intent.getComponent();
- if (cn != null && mActivityOverride.containsKey(cn.getPackageName())) {
- LauncherActivityInfo replacementInfo = mActivityOverride.get(cn.getPackageName());
+ ComponentName cn = intent.getComponent();
+ if (cn != null && type == ITEM_TYPE_APPLICATION
+ && !mValues.containsKey(Favorites.PROFILE_ID)) {
+ LauncherActivityInfo replacementInfo = mActivityOverride.get(cn.getPackageName());
+ if (replacementInfo != null) {
mValues.put(Favorites.PROFILE_ID, UserCache.INSTANCE.get(mContext)
.getSerialNumberForUser(replacementInfo.getUser()));
mValues.put(Favorites.INTENT, AppInfo.makeLaunchIntent(replacementInfo).toUri(0));
@@ -420,11 +437,7 @@
}
mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON);
- final Intent intent = new Intent(Intent.ACTION_MAIN, null)
- .addCategory(Intent.CATEGORY_LAUNCHER)
- .setComponent(new ComponentName(packageName, className))
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ Intent intent = AppInfo.makeLaunchIntent(new ComponentName(packageName, className));
return addShortcut(mContext.getString(R.string.package_state_unknown), intent,
ITEM_TYPE_APPLICATION);
}
diff --git a/src/com/android/launcher3/model/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
index 132b606..8368256 100644
--- a/src/com/android/launcher3/model/DatabaseHelper.java
+++ b/src/com/android/launcher3/model/DatabaseHelper.java
@@ -505,7 +505,7 @@
public int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
// TODO: Use multiple loaders with fall-back and transaction.
- int count = loader.loadLayout(db, new IntArray());
+ int count = loader.loadLayout(db);
// Ensure that the max ids are initialized
mMaxItemId = initializeMaxItemId(db);
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 102d28a..0d40a24 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -171,7 +171,7 @@
mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
mUserManager = mApp.getContext().getSystemService(UserManager.class);
mUserCache = UserCache.INSTANCE.get(mApp.getContext());
- mPmHelper = new PackageManagerHelper(mApp.getContext());
+ mPmHelper = PackageManagerHelper.INSTANCE.get(mApp.getContext());
mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
mIconCache = mApp.getIconCache();
mUserManagerState = userManagerState;
diff --git a/src/com/android/launcher3/model/UserManagerState.java b/src/com/android/launcher3/model/UserManagerState.java
index 720f08e..ed32430 100644
--- a/src/com/android/launcher3/model/UserManagerState.java
+++ b/src/com/android/launcher3/model/UserManagerState.java
@@ -17,6 +17,7 @@
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseBooleanArray;
@@ -27,6 +28,8 @@
*/
public class UserManagerState {
+ private static final String TAG = "UserManagerState";
+
public final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
private final LongSparseArray<Boolean> mQuietUsersSerialNoMap = new LongSparseArray<>();
@@ -39,6 +42,13 @@
for (UserHandle user : userManager.getUserProfiles()) {
long serialNo = userCache.getSerialNumberForUser(user);
boolean isUserQuiet = userManager.isQuietModeEnabled(user);
+ // Mapping different UserHandles to the same serialNo in allUsers could lead to losing
+ // UserHandle and cause a series of problems, such as incorrectly marking app as
+ // disabled and deleting app icons from workspace.
+ if (allUsers.get(serialNo) != null) {
+ Log.w(TAG, String.format("Override allUsers[%d]=%s with %s",
+ serialNo, allUsers.get(serialNo), user));
+ }
allUsers.put(serialNo, user);
mQuietUsersHashCodeMap.put(user.hashCode(), isUserQuiet);
mQuietUsersSerialNoMap.put(serialNo, isUserQuiet);
diff --git a/src/com/android/launcher3/util/LauncherLayoutBuilder.kt b/src/com/android/launcher3/util/LauncherLayoutBuilder.kt
new file mode 100644
index 0000000..ecc9953
--- /dev/null
+++ b/src/com/android/launcher3/util/LauncherLayoutBuilder.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2019 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.launcher3.util
+
+import android.util.Xml
+import com.android.launcher3.AutoInstallsLayout.ATTR_CLASS_NAME
+import com.android.launcher3.AutoInstallsLayout.ATTR_CONTAINER
+import com.android.launcher3.AutoInstallsLayout.ATTR_PACKAGE_NAME
+import com.android.launcher3.AutoInstallsLayout.ATTR_RANK
+import com.android.launcher3.AutoInstallsLayout.ATTR_SCREEN
+import com.android.launcher3.AutoInstallsLayout.ATTR_SHORTCUT_ID
+import com.android.launcher3.AutoInstallsLayout.ATTR_SPAN_X
+import com.android.launcher3.AutoInstallsLayout.ATTR_SPAN_Y
+import com.android.launcher3.AutoInstallsLayout.ATTR_TITLE
+import com.android.launcher3.AutoInstallsLayout.ATTR_TITLE_TEXT
+import com.android.launcher3.AutoInstallsLayout.ATTR_USER_TYPE
+import com.android.launcher3.AutoInstallsLayout.ATTR_X
+import com.android.launcher3.AutoInstallsLayout.ATTR_Y
+import com.android.launcher3.AutoInstallsLayout.TAG_APPWIDGET
+import com.android.launcher3.AutoInstallsLayout.TAG_AUTO_INSTALL
+import com.android.launcher3.AutoInstallsLayout.TAG_FOLDER
+import com.android.launcher3.AutoInstallsLayout.TAG_SHORTCUT
+import com.android.launcher3.AutoInstallsLayout.TAG_WORKSPACE
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.containerToString
+import java.io.IOException
+import java.io.StringWriter
+import java.io.Writer
+import org.xmlpull.v1.XmlSerializer
+
+/** Helper class to build xml for Launcher Layout */
+class LauncherLayoutBuilder {
+ private val nodes = ArrayList<Node>()
+
+ fun atHotseat(rank: Int) =
+ ItemTarget(
+ mapOf(
+ ATTR_CONTAINER to containerToString(CONTAINER_HOTSEAT),
+ ATTR_RANK to rank.toString()
+ )
+ )
+
+ fun atWorkspace(x: Int, y: Int, screen: Int) =
+ ItemTarget(
+ mapOf(
+ ATTR_CONTAINER to containerToString(CONTAINER_DESKTOP),
+ ATTR_X to x.toString(),
+ ATTR_Y to y.toString(),
+ ATTR_SCREEN to screen.toString()
+ )
+ )
+
+ @Throws(IOException::class) fun build() = StringWriter().apply { build(this) }.toString()
+
+ @Throws(IOException::class)
+ fun build(writer: Writer) {
+ Xml.newSerializer().apply {
+ setOutput(writer)
+ setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
+ startDocument("UTF-8", true)
+ startTag(null, TAG_WORKSPACE)
+ writeNodes(nodes)
+ endTag(null, TAG_WORKSPACE)
+ endDocument()
+ flush()
+ }
+ }
+
+ open inner class ItemTarget(private val baseValues: Map<String, String>) {
+ @JvmOverloads
+ fun putApp(packageName: String, className: String?, userType: String? = null) =
+ addItem(
+ TAG_AUTO_INSTALL,
+ userType,
+ mapOf(
+ ATTR_PACKAGE_NAME to packageName,
+ ATTR_CLASS_NAME to (className ?: packageName)
+ )
+ )
+
+ @JvmOverloads
+ fun putShortcut(packageName: String, shortcutId: String, userType: String? = null) =
+ addItem(
+ TAG_SHORTCUT,
+ userType,
+ mapOf(ATTR_PACKAGE_NAME to packageName, ATTR_SHORTCUT_ID to shortcutId)
+ )
+
+ @JvmOverloads
+ fun putWidget(
+ packageName: String,
+ className: String,
+ spanX: Int,
+ spanY: Int,
+ userType: String? = null
+ ) =
+ addItem(
+ TAG_APPWIDGET,
+ userType,
+ mapOf(
+ ATTR_PACKAGE_NAME to packageName,
+ ATTR_CLASS_NAME to className,
+ ATTR_SPAN_X to spanX.toString(),
+ ATTR_SPAN_Y to spanY.toString()
+ )
+ )
+
+ fun putFolder(titleResId: Int) = putFolder(ATTR_TITLE, titleResId.toString())
+
+ fun putFolder(title: String?) = putFolder(ATTR_TITLE_TEXT, title)
+
+ protected open fun addItem(
+ tag: String,
+ userType: String?,
+ props: Map<String, String>,
+ children: List<Node>? = null
+ ): LauncherLayoutBuilder {
+ nodes.add(
+ Node(
+ tag,
+ HashMap(baseValues).apply {
+ putAll(props)
+ userType?.let { put(ATTR_USER_TYPE, it) }
+ },
+ children
+ )
+ )
+ return this@LauncherLayoutBuilder
+ }
+
+ protected open fun putFolder(titleKey: String, titleValue: String?): FolderBuilder {
+ val folderBuilder = FolderBuilder()
+ addItem(TAG_FOLDER, null, mapOf(titleKey to (titleValue ?: "")), folderBuilder.children)
+ return folderBuilder
+ }
+ }
+
+ inner class FolderBuilder : ItemTarget(mapOf()) {
+
+ val children = ArrayList<Node>()
+
+ fun addApp(packageName: String, className: String?): FolderBuilder {
+ putApp(packageName, className)
+ return this
+ }
+
+ fun addShortcut(packageName: String, shortcutId: String): FolderBuilder {
+ putShortcut(packageName, shortcutId)
+ return this
+ }
+
+ override fun addItem(
+ tag: String,
+ userType: String?,
+ props: Map<String, String>,
+ childrenIgnored: List<Node>?
+ ): LauncherLayoutBuilder {
+ children.add(
+ Node(tag, HashMap(props).apply { userType?.let { put(ATTR_USER_TYPE, it) } })
+ )
+ return this@LauncherLayoutBuilder
+ }
+
+ override fun putFolder(titleKey: String, titleValue: String?): FolderBuilder {
+ throw IllegalArgumentException("Can't have folder inside a folder")
+ }
+
+ fun build() = this@LauncherLayoutBuilder
+ }
+
+ @Throws(IOException::class)
+ private fun XmlSerializer.writeNodes(nodes: List<Node>) {
+ nodes.forEach { node ->
+ startTag(null, node.name)
+ node.attrs.forEach { (key, value) -> attribute(null, key, value) }
+ node.children?.let { writeNodes(it) }
+ endTag(null, node.name)
+ }
+ }
+
+ data class Node(
+ val name: String,
+ val attrs: Map<String, String>,
+ val children: List<Node>? = null
+ )
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
new file mode 100644
index 0000000..b04bcca
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.launcher3
+
+import android.content.ComponentName
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import android.os.Process.myUserHandle
+import android.os.UserHandle
+import android.util.Xml
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback
+import com.android.launcher3.AutoInstallsLayout.SourceResources
+import com.android.launcher3.AutoInstallsLayout.TAG_WORKSPACE
+import com.android.launcher3.AutoInstallsLayout.USER_TYPE_WORK
+import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_PROVIDER
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.INTENT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID
+import com.android.launcher3.LauncherSettings.Favorites.SPANX
+import com.android.launcher3.LauncherSettings.Favorites.SPANY
+import com.android.launcher3.LauncherSettings.Favorites._ID
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.ApiWrapper
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LauncherLayoutBuilder
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.TestUtil
+import com.android.launcher3.util.UserIconInfo
+import com.android.launcher3.util.UserIconInfo.TYPE_MAIN
+import com.android.launcher3.util.UserIconInfo.TYPE_WORK
+import com.android.launcher3.widget.LauncherWidgetHolder
+import com.google.common.truth.Truth.assertThat
+import java.io.StringReader
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+/** Tests for [AutoInstallsLayout] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AutoInstallsLayoutTest {
+
+ lateinit var modelHelper: LauncherModelHelper
+ lateinit var targetContext: SandboxModelContext
+
+ lateinit var callback: MyCallback
+
+ @Mock lateinit var widgetHolder: LauncherWidgetHolder
+ @Mock lateinit var db: SQLiteDatabase
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ modelHelper = LauncherModelHelper()
+ targetContext = modelHelper.sandboxContext
+ callback = MyCallback()
+ }
+
+ @After
+ fun tearDown() {
+ modelHelper.destroy()
+ }
+
+ @Test
+ fun pending_icon_added_on_home() {
+ LauncherLayoutBuilder()
+ .atWorkspace(1, 1, 0)
+ .putApp("p1", "c1")
+ .toAutoInstallsLayout()
+ .loadLayout(db)
+
+ assertThat(callback.items.size).isEqualTo(1)
+ assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPLICATION)
+ assertThat(callback.items[0][INTENT])
+ .isEqualTo(AppInfo.makeLaunchIntent(ComponentName("p1", "c1")).toUri(0))
+ assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_DESKTOP)
+ assertThat(callback.items[0].containsKey(PROFILE_ID)).isFalse()
+ }
+
+ @Test
+ fun pending_icon_added_on_hotseat() {
+ LauncherLayoutBuilder()
+ .atHotseat(1)
+ .putApp("p1", "c1")
+ .toAutoInstallsLayout()
+ .loadLayout(db)
+
+ assertThat(callback.items.size).isEqualTo(1)
+ assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPLICATION)
+ assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_HOTSEAT)
+ }
+
+ @Test
+ fun widget_added_to_home() {
+ LauncherLayoutBuilder()
+ .atWorkspace(1, 1, 0)
+ .putWidget("p1", "c1", 2, 3)
+ .toAutoInstallsLayout()
+ .loadLayout(db)
+
+ assertThat(callback.items.size).isEqualTo(1)
+ assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPWIDGET)
+ assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_DESKTOP)
+ assertThat(callback.items[0][APPWIDGET_PROVIDER])
+ .isEqualTo(ComponentName("p1", "c1").flattenToString())
+ assertThat(callback.items[0][SPANX]).isEqualTo(2.toString())
+ assertThat(callback.items[0][SPANY]).isEqualTo(3.toString())
+ }
+
+ @Test
+ fun items_added_to_folder() {
+ LauncherLayoutBuilder()
+ .atHotseat(1)
+ .putFolder("Test")
+ .addApp("p1", "c")
+ .addApp("p2", "c")
+ .addApp("p3", "c")
+ .build()
+ .toAutoInstallsLayout()
+ .loadLayout(db)
+
+ assertThat(callback.items.size).isEqualTo(4)
+ assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_FOLDER)
+ assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_HOTSEAT)
+
+ val folderId = callback.items[0][_ID]
+ assertThat(callback.items[1][CONTAINER]).isEqualTo(folderId)
+ assertThat(callback.items[2][CONTAINER]).isEqualTo(folderId)
+ assertThat(callback.items[3][CONTAINER]).isEqualTo(folderId)
+ }
+
+ @Test
+ fun work_item_added_to_home() {
+ val apiWrapperMock = spy(ApiWrapper.INSTANCE[targetContext])
+ targetContext.putObject(ApiWrapper.INSTANCE, apiWrapperMock)
+ doReturn(
+ mapOf(
+ myUserHandle() to UserIconInfo(myUserHandle(), TYPE_MAIN, 0),
+ UserHandle.of(20) to UserIconInfo(UserHandle.of(20), TYPE_WORK, 20),
+ )
+ )
+ .whenever(apiWrapperMock)
+ .queryAllUsers()
+
+ val cache = UserCache.getInstance(targetContext)
+ TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+ assertThat(cache.userProfiles.size).isEqualTo(2)
+ }
+
+ LauncherLayoutBuilder()
+ .atWorkspace(1, 1, 0)
+ .putApp("p1", "c1", USER_TYPE_WORK)
+ .toAutoInstallsLayout()
+ .loadLayout(db)
+
+ assertThat(callback.items.size).isEqualTo(1)
+ assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPLICATION)
+ assertThat(callback.items[0][INTENT])
+ .isEqualTo(AppInfo.makeLaunchIntent(ComponentName("p1", "c1")).toUri(0))
+ assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_DESKTOP)
+ assertThat(callback.items[0][PROFILE_ID]).isEqualTo(20)
+ }
+
+ private fun LauncherLayoutBuilder.toAutoInstallsLayout() =
+ AutoInstallsLayout(
+ targetContext,
+ widgetHolder,
+ callback,
+ SourceResources.wrap(targetContext.resources),
+ { Xml.newPullParser().also { it.setInput(StringReader(build())) } },
+ TAG_WORKSPACE
+ )
+
+ class MyCallback : LayoutParserCallback {
+
+ val items = ArrayList<ContentValues>()
+
+ override fun generateNewItemId() = items.size
+
+ override fun insertAndCheck(db: SQLiteDatabase?, values: ContentValues): Int {
+ val id = values[_ID]
+ items.add(ContentValues(values))
+ return if (id is Int) id else 0
+ }
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
deleted file mode 100644
index ba01b04..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/**
- * Copyright (C) 2019 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.launcher3.util;
-
-
-import android.text.TextUtils;
-import android.util.Pair;
-import android.util.Xml;
-
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Helper class to build xml for Launcher Layout
- */
-public class LauncherLayoutBuilder {
-
- // Object Tags
- private static final String TAG_WORKSPACE = "workspace";
- private static final String TAG_AUTO_INSTALL = "autoinstall";
- private static final String TAG_FOLDER = "folder";
- private static final String TAG_APPWIDGET = "appwidget";
- private static final String TAG_SHORTCUT = "shortcut";
- private static final String TAG_EXTRA = "extra";
-
- private static final String ATTR_CONTAINER = "container";
- private static final String ATTR_RANK = "rank";
-
- private static final String ATTR_PACKAGE_NAME = "packageName";
- private static final String ATTR_CLASS_NAME = "className";
- private static final String ATTR_TITLE = "title";
- private static final String ATTR_TITLE_TEXT = "titleText";
- private static final String ATTR_SCREEN = "screen";
- private static final String ATTR_SHORTCUT_ID = "shortcutId";
-
- // x and y can be specified as negative integers, in which case -1 represents the
- // last row / column, -2 represents the second last, and so on.
- private static final String ATTR_X = "x";
- private static final String ATTR_Y = "y";
- private static final String ATTR_SPAN_X = "spanX";
- private static final String ATTR_SPAN_Y = "spanY";
-
- private static final String ATTR_CHILDREN = "children";
-
-
- // Style attrs -- "Extra"
- private static final String ATTR_KEY = "key";
- private static final String ATTR_VALUE = "value";
-
- private static final String CONTAINER_DESKTOP = "desktop";
- private static final String CONTAINER_HOTSEAT = "hotseat";
-
- private final ArrayList<Pair<String, HashMap<String, Object>>> mNodes = new ArrayList<>();
-
- public Location atHotseat(int rank) {
- Location l = new Location();
- l.items.put(ATTR_CONTAINER, CONTAINER_HOTSEAT);
- l.items.put(ATTR_RANK, Integer.toString(rank));
- return l;
- }
-
- public Location atWorkspace(int x, int y, int screen) {
- Location l = new Location();
- l.items.put(ATTR_CONTAINER, CONTAINER_DESKTOP);
- l.items.put(ATTR_X, Integer.toString(x));
- l.items.put(ATTR_Y, Integer.toString(y));
- l.items.put(ATTR_SCREEN, Integer.toString(screen));
- return l;
- }
-
- public String build() throws IOException {
- StringWriter writer = new StringWriter();
- build(writer);
- return writer.toString();
- }
-
- public void build(Writer writer) throws IOException {
- XmlSerializer serializer = Xml.newSerializer();
- serializer.setOutput(writer);
-
- serializer.startDocument("UTF-8", true);
- serializer.startTag(null, TAG_WORKSPACE);
- writeNodes(serializer, mNodes);
- serializer.endTag(null, TAG_WORKSPACE);
- serializer.endDocument();
- serializer.flush();
- }
-
- private static void writeNodes(XmlSerializer serializer,
- ArrayList<Pair<String, HashMap<String, Object>>> nodes) throws IOException {
- for (Pair<String, HashMap<String, Object>> node : nodes) {
- ArrayList<Pair<String, HashMap<String, Object>>> children = null;
-
- serializer.startTag(null, node.first);
- for (Map.Entry<String, Object> attr : node.second.entrySet()) {
- if (ATTR_CHILDREN.equals(attr.getKey())) {
- children = (ArrayList<Pair<String, HashMap<String, Object>>>) attr.getValue();
- } else {
- serializer.attribute(null, attr.getKey(), (String) attr.getValue());
- }
- }
-
- if (children != null) {
- writeNodes(serializer, children);
- }
- serializer.endTag(null, node.first);
- }
- }
-
- public class Location {
-
- final HashMap<String, Object> items = new HashMap<>();
-
- public LauncherLayoutBuilder putApp(String packageName, String className) {
- items.put(ATTR_PACKAGE_NAME, packageName);
- items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
- mNodes.add(Pair.create(TAG_AUTO_INSTALL, items));
- return LauncherLayoutBuilder.this;
- }
-
- public LauncherLayoutBuilder putShortcut(String packageName, String shortcutId) {
- items.put(ATTR_PACKAGE_NAME, packageName);
- items.put(ATTR_SHORTCUT_ID, shortcutId);
- mNodes.add(Pair.create(TAG_SHORTCUT, items));
- return LauncherLayoutBuilder.this;
- }
-
- public LauncherLayoutBuilder putWidget(String packageName, String className,
- int spanX, int spanY) {
- items.put(ATTR_PACKAGE_NAME, packageName);
- items.put(ATTR_CLASS_NAME, className);
- items.put(ATTR_SPAN_X, Integer.toString(spanX));
- items.put(ATTR_SPAN_Y, Integer.toString(spanY));
- mNodes.add(Pair.create(TAG_APPWIDGET, items));
- return LauncherLayoutBuilder.this;
- }
-
- public FolderBuilder putFolder(int titleResId) {
- items.put(ATTR_TITLE, Integer.toString(titleResId));
- return putFolder();
- }
-
- public FolderBuilder putFolder(String title) {
- items.put(ATTR_TITLE_TEXT, title);
- return putFolder();
- }
-
- private FolderBuilder putFolder() {
- FolderBuilder folderBuilder = new FolderBuilder();
- items.put(ATTR_CHILDREN, folderBuilder.mChildren);
- mNodes.add(Pair.create(TAG_FOLDER, items));
- return folderBuilder;
- }
- }
-
- public class FolderBuilder {
-
- final ArrayList<Pair<String, HashMap<String, Object>>> mChildren = new ArrayList<>();
-
- public FolderBuilder addApp(String packageName, String className) {
- HashMap<String, Object> items = new HashMap<>();
- items.put(ATTR_PACKAGE_NAME, packageName);
- items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
- mChildren.add(Pair.create(TAG_AUTO_INSTALL, items));
- return this;
- }
-
- public FolderBuilder addShortcut(String packageName, String shortcutId) {
- HashMap<String, Object> items = new HashMap<>();
- items.put(ATTR_PACKAGE_NAME, packageName);
- items.put(ATTR_SHORTCUT_ID, shortcutId);
- mChildren.add(Pair.create(TAG_SHORTCUT, items));
- return this;
- }
-
- public LauncherLayoutBuilder build() {
- return LauncherLayoutBuilder.this;
- }
- }
-}
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index 6bbcf85..6cf3b19 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -23,6 +23,7 @@
import android.content.pm.LauncherApps
import android.content.pm.PackageInstaller
import android.content.pm.ShortcutInfo
+import android.os.Process
import android.os.UserHandle
import android.platform.test.annotations.EnableFlags
import android.util.LongSparseArray
@@ -429,6 +430,7 @@
whenever(disabledMessage).thenReturn("")
whenever(disabledReason).thenReturn(0)
whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
+ whenever(userHandle).thenReturn(Process.myUserHandle())
}
mIconRequestInfos = mutableListOf()
// Make sure shortcuts map has expected key from expected package