/* * 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 androidx.annotation.VisibleForTesting 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_APP_PAIR 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.GridSizeUtil.Companion.COLUMN_ATTR import com.android.launcher3.GridSizeUtil.Companion.ROW_ATTR 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 rows: Int? = null, private val columns: Int? = null) { private val nodes = ArrayList() 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(), ) ) @VisibleForTesting fun withBaseValues(baseValues: Map) = ItemTarget(baseValues) @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) rows?.let { attribute(null, ROW_ATTR, it.toString()) } columns?.let { attribute(null, COLUMN_ATTR, it.toString()) } writeNodes(nodes) endTag(null, TAG_WORKSPACE) endDocument() flush() } } open inner class ItemTarget(private val baseValues: Map) { @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) open fun putAppPair(title: String) = AppPairBuilder().also { addItem(TAG_APP_PAIR, null, mapOf(ATTR_TITLE_TEXT to title), it.childNodes) } protected open fun addItem( tag: String, userType: String?, props: Map, children: List? = 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().also { addItem(TAG_FOLDER, null, mapOf(titleKey to (titleValue ?: "")), it.childNodes) } } open inner class FolderBuilder : ItemTarget(mapOf()) { val childNodes = ArrayList() 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, children: List?, ): LauncherLayoutBuilder { childNodes.add( Node( tag, HashMap(props).apply { userType?.let { put(ATTR_USER_TYPE, it) } }, children, ) ) return this@LauncherLayoutBuilder } override fun putFolder(titleKey: String, titleValue: String?): FolderBuilder { throw IllegalArgumentException("Can't have folder inside a $javaClass") } fun build() = this@LauncherLayoutBuilder } inner class AppPairBuilder : FolderBuilder() { override fun putAppPair(title: String): AppPairBuilder { throw IllegalArgumentException("Can't have app pair inside an app pair") } override fun addItem( tag: String, userType: String?, props: Map, children: List?, ): LauncherLayoutBuilder { if (childNodes.size == 2) { throw IllegalStateException("can't have more than 2 items in an app pair") } return super.addItem(tag, userType, props, children) } } @Throws(IOException::class) private fun XmlSerializer.writeNodes(nodes: List) { 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, val children: List? = null, ) }