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