Add LauncherProvider interface to get and set current workspace layout via xml
=> For prototyping purposes only. The method is protected by system|signature permission.
=> Uses the AutoInstall layout xml specification to allow for getting and setting the current launcher layout.
Test: manual via prototype apk
Flag: EXEMPT prototyping interface
Change-Id: I04dd29ee69db642095dfb5f6c4965cdb8509b05e
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 417bb74..7c09e9a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -17,8 +17,6 @@
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
@@ -29,10 +27,8 @@
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.view.inputmethod.EditorInfo
@@ -44,33 +40,16 @@
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_LABEL
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
-import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
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.LayoutImportExportHelper
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
@@ -80,14 +59,12 @@
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
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.nio.charset.StandardCharsets
import java.util.Locale
import java.util.concurrent.Executor
@@ -421,26 +398,12 @@
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)
- }
-
+ LayoutImportExportHelper.exportModelDbAsXml(context) { layoutXml ->
context.contentResolver.openOutputStream(uri).use { os ->
- builder.build(OutputStreamWriter(os))
+ val bytes: ByteArray =
+ layoutXml.toByteArray(StandardCharsets.UTF_8) // Encode to UTF-8
+ os?.write(bytes)
}
-
MAIN_EXECUTOR.execute {
Toast.makeText(context, "File saved", Toast.LENGTH_LONG).show()
}
@@ -458,66 +421,12 @@
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) {
- Secure.putString(
- resolver,
- LAYOUT_PROVIDER_KEY,
- createBlobProviderKey(digest),
- )
-
- MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
- MAIN_EXECUTOR.submit { model.forceReload() }.get()
- MODEL_EXECUTOR.submit {}.get()
- Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
- }
- }
+ LayoutImportExportHelper.importModelFromXml(context, data)
}
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,
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 6e2d357..a526b89 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -24,25 +24,38 @@
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.util.LayoutImportExportHelper;
import com.android.launcher3.widget.LauncherWidgetHolder;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.function.ToIntFunction;
public class LauncherProvider extends ContentProvider {
private static final String TAG = "LauncherProvider";
+ // Method API For Provider#call method.
+ private static final String METHOD_EXPORT_LAYOUT_XML = "EXPORT_LAYOUT_XML";
+ private static final String METHOD_IMPORT_LAYOUT_XML = "IMPORT_LAYOUT_XML";
+ private static final String KEY_RESULT = "KEY_RESULT";
+ private static final String KEY_LAYOUT = "KEY_LAYOUT";
+ private static final String SUCCESS = "success";
+ private static final String FAILURE = "failure";
+
/**
* $ adb shell dumpsys activity provider com.android.launcher3
*/
@@ -142,6 +155,43 @@
return executeControllerTask(c -> c.update(args.table, values, args.where, args.args));
}
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ Bundle b = new Bundle();
+
+ // The caller must have the read or write permission for this content provider to
+ // access the "call" method at all. We also enforce the appropriate per-method permissions.
+ switch(method) {
+ case METHOD_EXPORT_LAYOUT_XML:
+ if (getContext().checkCallingOrSelfPermission(getReadPermission())
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller doesn't have read permission");
+ }
+
+ CompletableFuture<String> resultFuture = LayoutImportExportHelper.INSTANCE
+ .exportModelDbAsXmlFuture(getContext());
+ try {
+ b.putString(KEY_LAYOUT, resultFuture.get());
+ b.putString(KEY_RESULT, SUCCESS);
+ } catch (ExecutionException | InterruptedException e) {
+ b.putString(KEY_RESULT, FAILURE);
+ }
+ return b;
+
+ case METHOD_IMPORT_LAYOUT_XML:
+ if (getContext().checkCallingOrSelfPermission(getWritePermission())
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller doesn't have write permission");
+ }
+
+ LayoutImportExportHelper.INSTANCE.importModelFromXml(getContext(), arg);
+ b.putString(KEY_RESULT, SUCCESS);
+ return b;
+ default:
+ return null;
+ }
+ }
+
private int executeControllerTask(ToIntFunction<ModelDbController> task) {
if (Binder.getCallingPid() == Process.myPid()) {
throw new IllegalArgumentException("Same process should call model directly");
diff --git a/src/com/android/launcher3/util/LayoutImportExportHelper.kt b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
new file mode 100644
index 0000000..4033f60
--- /dev/null
+++ b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.util
+
+import android.app.blob.BlobHandle.createWithSha256
+import android.app.blob.BlobStoreManager
+import android.content.Context
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import android.provider.Settings.Secure
+import com.android.launcher3.AutoInstallsLayout
+import com.android.launcher3.LauncherAppState
+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_LABEL
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
+import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
+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.shortcuts.ShortcutKey
+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 java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+import java.util.concurrent.CompletableFuture
+
+object LayoutImportExportHelper {
+ fun exportModelDbAsXmlFuture(context: Context): CompletableFuture<String> {
+ val future = CompletableFuture<String>()
+ exportModelDbAsXml(context) { xmlString -> future.complete(xmlString) }
+ return future
+ }
+
+ fun exportModelDbAsXml(context: Context, callback: (String) -> Unit) {
+ val model = LauncherAppState.getInstance(context).model
+
+ 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(context, info)
+ }
+ dataModel.appWidgets.forEach { info ->
+ builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(context, info)
+ }
+
+ val layoutXml = builder.build()
+ callback(layoutXml)
+ }
+ }
+
+ fun importModelFromXml(context: Context, xmlString: String) {
+ importModelFromXml(context, xmlString.toByteArray(StandardCharsets.UTF_8))
+ }
+
+ fun importModelFromXml(context: Context, data: ByteArray) {
+ val model = LauncherAppState.getInstance(context).model
+
+ 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)!!
+
+ val resolver = context.contentResolver
+
+ blobManager.openSession(blobManager.createSession(handle)).use { session ->
+ AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
+ session.allowPublicAccess()
+
+ session.commit(ORDERED_BG_EXECUTOR) {
+ Secure.putString(resolver, LAYOUT_PROVIDER_KEY, createBlobProviderKey(digest))
+
+ MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
+ MAIN_EXECUTOR.submit { model.forceReload() }.get()
+ MODEL_EXECUTOR.submit {}.get()
+ Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
+ }
+ }
+ }
+
+ private fun LauncherLayoutBuilder.ItemTarget.addItem(context: Context, 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(context, folderContent)
+ }
+ }
+ }
+ ITEM_TYPE_APPWIDGET ->
+ putWidget(
+ (info as LauncherAppWidgetInfo).providerName.packageName,
+ info.providerName.className,
+ info.spanX,
+ info.spanY,
+ userType,
+ )
+ }
+ }
+}
\ No newline at end of file