SetupWizard: OEM-contributed wizard pages from the config APK
A config APK can ship res/xml/oem_wizard.xml (declarative Rich schema: section/text/image/toggle/choice/input/link, multi-page). ConfigProvisioner emits Settings.Global pawlet.oem_wizard_pkg when it detects the file; the wizard loads + parses it and appends the pages near the end of the flow. - lib/oem: OemModels, OemWizardParser (pull-parser; text carried in a value attribute since compiled res/xml drops element text), OemWizardRepository (signal + default-package fallback), OemPendingStore (deferred secure/global writes). - OemWizardScreen renders elements to Compose on the shared scaffold; toggle/ choice/input selections are seeded with OEM defaults and recorded, then applied in finishSetupWizard. - MainActivity appends OEM pages; no config-APK code runs (declarative only), link actions limited to explicit component:/intent:.
This commit is contained in:
@@ -14,10 +14,13 @@ import me.pawlet.setupwizard.lib.FullScreenHelper
|
||||
import me.pawlet.setupwizard.lib.BaseKioskActivity
|
||||
import me.pawlet.setupwizard.lib.SetupWizardManager
|
||||
import me.pawlet.setupwizard.lib.WizardFlags
|
||||
import me.pawlet.setupwizard.lib.WizardPage
|
||||
import me.pawlet.setupwizard.lib.buildBuiltinPages
|
||||
import me.pawlet.setupwizard.lib.oem.OemWizardRepository
|
||||
import me.pawlet.setupwizard.lib.utils.SetupUtils
|
||||
import me.pawlet.setupwizard.ui.screens.AboutDeviceScreen
|
||||
import me.pawlet.setupwizard.ui.screens.AndroidVersionScreen
|
||||
import me.pawlet.setupwizard.ui.screens.wizard.OemWizardScreen
|
||||
import me.pawlet.setupwizard.ui.theme.MainTheme
|
||||
|
||||
class MainActivity : BaseKioskActivity() {
|
||||
@@ -39,14 +42,26 @@ class MainActivity : BaseKioskActivity() {
|
||||
}
|
||||
|
||||
wizardManager = SetupWizardManager.getInstance(this)
|
||||
wizardManager.registerPages(
|
||||
buildBuiltinPages(
|
||||
onFinish = { finish() },
|
||||
onSecretUnlocked = {
|
||||
wizardManager.showOverlay(SetupWizardManager.Overlay.AboutDevice)
|
||||
|
||||
val builtinPages = buildBuiltinPages(
|
||||
onFinish = { finish() },
|
||||
onSecretUnlocked = {
|
||||
wizardManager.showOverlay(SetupWizardManager.Overlay.AboutDevice)
|
||||
}
|
||||
)
|
||||
// OEM pages contributed by the config APK (ConfigProvisioner emits the
|
||||
// signal; OemWizardRepository loads + parses the declarative XML). They
|
||||
// land near the end of the flow, before "complete".
|
||||
val oemPages = OemWizardRepository.load(this).mapIndexed { i, page ->
|
||||
WizardPage(
|
||||
id = "oem_${page.id}",
|
||||
order = 92 + i,
|
||||
content = { onNext, onBack ->
|
||||
OemWizardScreen(page = page, onContinue = onNext, onBack = onBack)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
wizardManager.registerPages(builtinPages + oemPages)
|
||||
|
||||
setContent {
|
||||
MainTheme {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package me.pawlet.setupwizard.lib.oem
|
||||
|
||||
/**
|
||||
* Parsed model of an OEM-provided wizard, loaded from a config APK's
|
||||
* res/xml/oem_wizard.xml. Purely declarative — no code from the config APK is
|
||||
* executed; the SetupWizard renders these to Compose.
|
||||
*/
|
||||
data class OemWizard(
|
||||
val version: Int,
|
||||
val pages: List<OemPage>
|
||||
)
|
||||
|
||||
data class OemPage(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val icon: String?, // logical icon name mapped by the renderer
|
||||
val elements: List<OemElement>
|
||||
)
|
||||
|
||||
/** A setting target: "secure:<key>" or "global:<key>". Null = no persistence. */
|
||||
typealias OemTarget = String
|
||||
|
||||
/** Value type stored for deferred application at finishSetupWizard. */
|
||||
enum class OemValueType { BOOL, STRING }
|
||||
|
||||
sealed interface OemElement {
|
||||
data class Section(val title: String) : OemElement
|
||||
data class Text(val text: String) : OemElement
|
||||
data class Image(val url: String) : OemElement
|
||||
|
||||
data class Toggle(
|
||||
val key: String,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val default: Boolean,
|
||||
val target: OemTarget?
|
||||
) : OemElement
|
||||
|
||||
data class Choice(
|
||||
val key: String,
|
||||
val title: String,
|
||||
val target: OemTarget?,
|
||||
val default: String?,
|
||||
val options: List<Option>
|
||||
) : OemElement {
|
||||
data class Option(val value: String, val label: String)
|
||||
}
|
||||
|
||||
data class Input(
|
||||
val key: String,
|
||||
val title: String,
|
||||
val hint: String?,
|
||||
val default: String?,
|
||||
val target: OemTarget?
|
||||
) : OemElement
|
||||
|
||||
/** action: "component:<pkg>/<cls>" or "intent:<ACTION>" (whitelisted). */
|
||||
data class Link(val label: String, val action: String) : OemElement
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package me.pawlet.setupwizard.lib.oem
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Deferred store for OEM page selections. Toggles/choices/inputs record their
|
||||
* chosen value here during the wizard (keyed by "secure:<key>" / "global:<key>"
|
||||
* target); [apply] writes them all to their real settings at
|
||||
* finishSetupWizard() and clears the store. Values are encoded with a 1-char
|
||||
* type marker: "B0"/"B1" for booleans, "S<text>" for strings.
|
||||
*/
|
||||
object OemPendingStore {
|
||||
|
||||
private const val PREFS = "oem_pending"
|
||||
private const val TAG = "OemPendingStore"
|
||||
|
||||
private fun prefs(context: Context) =
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
fun setBool(context: Context, target: String, value: Boolean) {
|
||||
prefs(context).edit().putString(target, if (value) "B1" else "B0").apply()
|
||||
}
|
||||
|
||||
fun setString(context: Context, target: String, value: String) {
|
||||
prefs(context).edit().putString(target, "S$value").apply()
|
||||
}
|
||||
|
||||
/** Seed a default only if the user hasn't set this target yet. */
|
||||
fun seedBool(context: Context, target: String, value: Boolean) {
|
||||
if (!prefs(context).contains(target)) setBool(context, target, value)
|
||||
}
|
||||
|
||||
fun seedString(context: Context, target: String, value: String) {
|
||||
if (!prefs(context).contains(target)) setString(context, target, value)
|
||||
}
|
||||
|
||||
fun currentBool(context: Context, target: String, def: Boolean): Boolean =
|
||||
prefs(context).getString(target, null)?.let { it == "B1" } ?: def
|
||||
|
||||
fun currentString(context: Context, target: String, def: String): String =
|
||||
prefs(context).getString(target, null)?.let {
|
||||
if (it.startsWith("S")) it.substring(1) else def
|
||||
} ?: def
|
||||
|
||||
/** Write all pending targets to their real settings, then clear. */
|
||||
fun apply(context: Context) {
|
||||
val cr = context.contentResolver
|
||||
val all = prefs(context).all
|
||||
for ((target, rawAny) in all) {
|
||||
val raw = rawAny as? String ?: continue
|
||||
val sep = target.indexOf(':')
|
||||
if (sep <= 0) continue
|
||||
val scope = target.substring(0, sep) // "secure" | "global"
|
||||
val key = target.substring(sep + 1)
|
||||
runCatching {
|
||||
when (raw.firstOrNull()) {
|
||||
'B' -> writeInt(cr, scope, key, if (raw == "B1") 1 else 0)
|
||||
'S' -> writeString(cr, scope, key, raw.substring(1))
|
||||
else -> {}
|
||||
}
|
||||
}.onFailure { Log.w(TAG, "apply $target failed", it) }
|
||||
}
|
||||
prefs(context).edit().clear().apply()
|
||||
}
|
||||
|
||||
private fun writeInt(cr: android.content.ContentResolver, scope: String, key: String, v: Int) {
|
||||
when (scope) {
|
||||
"secure" -> Settings.Secure.putInt(cr, key, v)
|
||||
"global" -> Settings.Global.putInt(cr, key, v)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeString(cr: android.content.ContentResolver, scope: String, key: String, v: String) {
|
||||
when (scope) {
|
||||
"secure" -> Settings.Secure.putString(cr, key, v)
|
||||
"global" -> Settings.Global.putString(cr, key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package me.pawlet.setupwizard.lib.oem
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
|
||||
/**
|
||||
* Parses res/xml/oem_wizard.xml (Rich schema) from a config APK into an
|
||||
* [OemWizard]. Declarative only; unknown tags/attributes are ignored so the
|
||||
* schema can grow without breaking older parsers.
|
||||
*/
|
||||
object OemWizardParser {
|
||||
|
||||
fun parse(parser: XmlPullParser): OemWizard {
|
||||
var version = 1
|
||||
val pages = mutableListOf<OemPage>()
|
||||
var event = parser.eventType
|
||||
while (event != XmlPullParser.END_DOCUMENT) {
|
||||
if (event == XmlPullParser.START_TAG) {
|
||||
when (parser.name) {
|
||||
"oemWizard" -> version = attrInt(parser, "version", 1)
|
||||
"page" -> pages.add(parsePage(parser))
|
||||
}
|
||||
}
|
||||
event = parser.next()
|
||||
}
|
||||
return OemWizard(version, pages)
|
||||
}
|
||||
|
||||
private fun parsePage(parser: XmlPullParser): OemPage {
|
||||
val id = attr(parser, "id").orEmpty()
|
||||
val title = attr(parser, "title").orEmpty()
|
||||
val subtitle = attr(parser, "subtitle")
|
||||
val icon = attr(parser, "icon")
|
||||
val elements = mutableListOf<OemElement>()
|
||||
|
||||
var event = parser.next()
|
||||
while (!(event == XmlPullParser.END_TAG && parser.name == "page") &&
|
||||
event != XmlPullParser.END_DOCUMENT
|
||||
) {
|
||||
if (event == XmlPullParser.START_TAG) {
|
||||
parseElement(parser)?.let(elements::add)
|
||||
}
|
||||
event = parser.next()
|
||||
}
|
||||
return OemPage(id, title, subtitle, icon, elements)
|
||||
}
|
||||
|
||||
private fun parseElement(parser: XmlPullParser): OemElement? = when (parser.name) {
|
||||
"section" -> OemElement.Section(attr(parser, "title").orEmpty())
|
||||
// Element text/CDATA is not preserved in compiled res/xml, so text
|
||||
// content is carried in the "value" attribute.
|
||||
"text" -> OemElement.Text(attr(parser, "value").orEmpty())
|
||||
"image" -> OemElement.Image(attr(parser, "url").orEmpty())
|
||||
"toggle" -> OemElement.Toggle(
|
||||
key = attr(parser, "key").orEmpty(),
|
||||
title = attr(parser, "title").orEmpty(),
|
||||
subtitle = attr(parser, "subtitle"),
|
||||
default = attrBool(parser, "default", false),
|
||||
target = attr(parser, "target")
|
||||
)
|
||||
"input" -> OemElement.Input(
|
||||
key = attr(parser, "key").orEmpty(),
|
||||
title = attr(parser, "title").orEmpty(),
|
||||
hint = attr(parser, "hint"),
|
||||
default = attr(parser, "default"),
|
||||
target = attr(parser, "target")
|
||||
)
|
||||
"link" -> OemElement.Link(
|
||||
label = attr(parser, "label").orEmpty(),
|
||||
action = attr(parser, "action").orEmpty()
|
||||
)
|
||||
"choice" -> parseChoice(parser)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun parseChoice(parser: XmlPullParser): OemElement.Choice {
|
||||
val key = attr(parser, "key").orEmpty()
|
||||
val title = attr(parser, "title").orEmpty()
|
||||
val target = attr(parser, "target")
|
||||
val default = attr(parser, "default")
|
||||
val options = mutableListOf<OemElement.Choice.Option>()
|
||||
|
||||
var event = parser.next()
|
||||
while (!(event == XmlPullParser.END_TAG && parser.name == "choice") &&
|
||||
event != XmlPullParser.END_DOCUMENT
|
||||
) {
|
||||
if (event == XmlPullParser.START_TAG && parser.name == "option") {
|
||||
options.add(
|
||||
OemElement.Choice.Option(
|
||||
value = attr(parser, "value").orEmpty(),
|
||||
label = attr(parser, "label").orEmpty()
|
||||
)
|
||||
)
|
||||
}
|
||||
event = parser.next()
|
||||
}
|
||||
return OemElement.Choice(key, title, target, default, options)
|
||||
}
|
||||
|
||||
private fun attr(parser: XmlPullParser, name: String): String? =
|
||||
parser.getAttributeValue(null, name)
|
||||
|
||||
private fun attrInt(parser: XmlPullParser, name: String, def: Int): Int =
|
||||
attr(parser, name)?.toIntOrNull() ?: def
|
||||
|
||||
private fun attrBool(parser: XmlPullParser, name: String, def: Boolean): Boolean =
|
||||
attr(parser, name)?.let { it == "true" || it == "1" } ?: def
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package me.pawlet.setupwizard.lib.oem
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
|
||||
/**
|
||||
* Loads OEM wizard pages from a config APK. Discovery order:
|
||||
* 1. Settings.Global "pawlet.oem_wizard_pkg" — set by ConfigProvisioner when it
|
||||
* detects res/xml/oem_wizard.xml in the (possibly OTA-updated) config APK.
|
||||
* 2. Fallback to the default config APK package, so the pages still appear on
|
||||
* the first boot before ConfigProvisioner's boot service has run.
|
||||
* Only a package that resolves + parses to non-empty pages is used.
|
||||
*/
|
||||
object OemWizardRepository {
|
||||
|
||||
/** Settings.Global key ConfigProvisioner writes with the OEM page package. */
|
||||
const val SIGNAL_KEY = "pawlet.oem_wizard_pkg"
|
||||
|
||||
private const val DEFAULT_CONFIG_PKG = "app.pawlet.config"
|
||||
private const val XML_NAME = "oem_wizard"
|
||||
|
||||
fun load(context: Context): List<OemPage> {
|
||||
val pkg = resolvePackage(context) ?: return emptyList()
|
||||
return runCatching {
|
||||
val res = context.packageManager.getResourcesForApplication(pkg)
|
||||
val xmlId = res.getIdentifier(XML_NAME, "xml", pkg)
|
||||
if (xmlId == 0) return emptyList()
|
||||
val parser = res.getXml(xmlId)
|
||||
try {
|
||||
OemWizardParser.parse(parser).pages
|
||||
} finally {
|
||||
parser.close()
|
||||
}
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
private fun resolvePackage(context: Context): String? {
|
||||
val signal = runCatching {
|
||||
Settings.Global.getString(context.contentResolver, SIGNAL_KEY)
|
||||
}.getOrNull()?.takeIf { it.isNotBlank() }
|
||||
val pkg = signal ?: DEFAULT_CONFIG_PKG
|
||||
return if (isInstalled(context, pkg)) pkg else null
|
||||
}
|
||||
|
||||
private fun isInstalled(context: Context, pkg: String): Boolean = runCatching {
|
||||
context.packageManager.getPackageInfo(pkg, 0)
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.core.content.edit
|
||||
import me.pawlet.setupwizard.lib.oem.OemPendingStore
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
@@ -589,7 +590,9 @@ class SetupUtils {
|
||||
WallpaperManager.getInstance(context).forgetLoadedWallpaper()
|
||||
disableHome(context)
|
||||
|
||||
// Restore the status bar that was locked for setup.
|
||||
// Apply deferred OEM page selections (config APK), then restore the
|
||||
// status bar that was locked for setup.
|
||||
OemPendingStore.apply(context)
|
||||
enableStatusBar(context)
|
||||
|
||||
// Notify partner/system apps (e.g. ConfigProvisioner) that setup is done
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package me.pawlet.setupwizard.ui.screens.wizard
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Business
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.Extension
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.filled.SignalCellularAlt
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import me.pawlet.setupwizard.lib.oem.OemElement
|
||||
import me.pawlet.setupwizard.lib.oem.OemPage
|
||||
import me.pawlet.setupwizard.lib.oem.OemPendingStore
|
||||
import me.pawlet.setupwizard.ui.components.WizardRadioOption
|
||||
import me.pawlet.setupwizard.ui.components.WizardSecondaryButton
|
||||
import me.pawlet.setupwizard.ui.components.WizardStepScaffold
|
||||
import me.pawlet.setupwizard.ui.components.WizardToggleCard
|
||||
|
||||
/**
|
||||
* Renders one OEM-provided [OemPage] (from the config APK) as a wizard step.
|
||||
* Toggle/choice/input selections are recorded in [OemPendingStore] (with the
|
||||
* OEM's defaults seeded on entry) and applied at finishSetupWizard. No code from
|
||||
* the config APK runs — this only interprets the declarative model.
|
||||
*/
|
||||
@Composable
|
||||
fun OemWizardScreen(
|
||||
page: OemPage,
|
||||
onContinue: () -> Unit,
|
||||
onBack: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// Seed OEM defaults so they apply even if the user doesn't interact.
|
||||
LaunchedEffect(page.id) {
|
||||
page.elements.forEach { el ->
|
||||
when (el) {
|
||||
is OemElement.Toggle -> el.target?.let { OemPendingStore.seedBool(context, it, el.default) }
|
||||
is OemElement.Choice -> if (el.target != null && el.default != null)
|
||||
OemPendingStore.seedString(context, el.target, el.default)
|
||||
is OemElement.Input -> if (el.target != null && el.default != null)
|
||||
OemPendingStore.seedString(context, el.target, el.default)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WizardStepScaffold(
|
||||
icon = iconFor(page.icon),
|
||||
title = page.title,
|
||||
subtitle = page.subtitle,
|
||||
onContinue = onContinue,
|
||||
onBack = onBack
|
||||
) {
|
||||
page.elements.forEach { el ->
|
||||
when (el) {
|
||||
is OemElement.Section -> Text(
|
||||
el.title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
is OemElement.Text -> Text(
|
||||
el.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f)
|
||||
)
|
||||
is OemElement.Image -> AsyncImage(
|
||||
model = el.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
is OemElement.Toggle -> OemToggle(el)
|
||||
is OemElement.Choice -> OemChoice(el)
|
||||
is OemElement.Input -> OemInput(el)
|
||||
is OemElement.Link -> WizardSecondaryButton(
|
||||
label = el.label,
|
||||
onClick = { launchOemAction(context, el.action) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OemToggle(el: OemElement.Toggle) {
|
||||
val context = LocalContext.current
|
||||
var checked by remember {
|
||||
mutableStateOf(el.target?.let { OemPendingStore.currentBool(context, it, el.default) } ?: el.default)
|
||||
}
|
||||
WizardToggleCard(
|
||||
title = el.title,
|
||||
subtitle = el.subtitle,
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
el.target?.let { t -> OemPendingStore.setBool(context, t, it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OemChoice(el: OemElement.Choice) {
|
||||
val context = LocalContext.current
|
||||
var selected by remember {
|
||||
mutableStateOf(
|
||||
el.target?.let { OemPendingStore.currentString(context, it, el.default.orEmpty()) }
|
||||
?: el.default.orEmpty()
|
||||
)
|
||||
}
|
||||
if (el.title.isNotEmpty()) {
|
||||
Text(el.title, style = MaterialTheme.typography.titleSmall)
|
||||
}
|
||||
el.options.forEach { opt ->
|
||||
WizardRadioOption(
|
||||
label = opt.label,
|
||||
selected = selected == opt.value,
|
||||
onSelect = {
|
||||
selected = opt.value
|
||||
el.target?.let { t -> OemPendingStore.setString(context, t, opt.value) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OemInput(el: OemElement.Input) {
|
||||
val context = LocalContext.current
|
||||
var value by remember {
|
||||
mutableStateOf(
|
||||
el.target?.let { OemPendingStore.currentString(context, it, el.default.orEmpty()) }
|
||||
?: el.default.orEmpty()
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = {
|
||||
value = it
|
||||
el.target?.let { t -> OemPendingStore.setString(context, t, it) }
|
||||
},
|
||||
label = { Text(el.title) },
|
||||
placeholder = el.hint?.let { { Text(it) } },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
private fun iconFor(name: String?): ImageVector = when (name) {
|
||||
"cellular" -> Icons.Filled.SignalCellularAlt
|
||||
"cloud" -> Icons.Filled.Cloud
|
||||
"settings" -> Icons.Filled.Settings
|
||||
"info" -> Icons.Filled.Info
|
||||
"star" -> Icons.Filled.Star
|
||||
"business" -> Icons.Filled.Business
|
||||
"shield" -> Icons.Filled.Shield
|
||||
else -> Icons.Filled.Extension
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a whitelisted action from a Link element. Only explicit component
|
||||
* targets ("component:pkg/cls") and explicit actions ("intent:ACTION") are
|
||||
* honoured; anything else is ignored.
|
||||
*/
|
||||
private fun launchOemAction(context: android.content.Context, action: String) {
|
||||
val intent = when {
|
||||
action.startsWith("component:") -> {
|
||||
val spec = action.removePrefix("component:")
|
||||
val slash = spec.indexOf('/')
|
||||
if (slash <= 0) return
|
||||
val pkg = spec.substring(0, slash)
|
||||
var cls = spec.substring(slash + 1)
|
||||
if (cls.startsWith(".")) cls = pkg + cls
|
||||
Intent().setClassName(pkg, cls)
|
||||
}
|
||||
action.startsWith("intent:") -> Intent(action.removePrefix("intent:"))
|
||||
else -> return
|
||||
}.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
runCatching { context.startActivity(intent) }
|
||||
}
|
||||
Reference in New Issue
Block a user