Mask private profile apps while private space is locked

This commit is contained in:
MM20 2024-08-13 15:19:24 +02:00
parent 74060d53c3
commit 318c26a439
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
9 changed files with 242 additions and 74 deletions

View File

@ -111,34 +111,43 @@ fun AppItem(
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
val tags by viewModel.tags.collectAsState(emptyList()) if (!app.isPrivate) {
if (tags.isNotEmpty()) {
Text( val tags by viewModel.tags.collectAsState(emptyList())
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp), if (tags.isNotEmpty()) {
text = tags.joinToString(separator = " #", prefix = "#"), Text(
color = MaterialTheme.colorScheme.secondary, modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
style = MaterialTheme.typography.labelSmall text = tags.joinToString(separator = " #", prefix = "#"),
) color = MaterialTheme.colorScheme.secondary,
} style = MaterialTheme.typography.labelSmall
)
}
app.versionName?.let { app.versionName?.let {
Text(
text = stringResource(R.string.app_info_version, it),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text( Text(
text = stringResource(R.string.app_info_version, it), text = app.componentName.packageName,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp), modifier = Modifier.padding(top = 1.dp),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis,
)
} else {
Text(
stringResource(R.string.profile_private_profile_state_locked),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp),
color = MaterialTheme.colorScheme.secondary,
) )
} }
Text(
text = app.componentName.packageName,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 1.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} }
val badge by viewModel.badge.collectAsStateWithLifecycle(null) val badge by viewModel.badge.collectAsStateWithLifecycle(null)
@ -373,13 +382,15 @@ fun AppItem(
toolbarActions.add(favAction) toolbarActions.add(favAction)
} }
toolbarActions.add( if (!app.isPrivate) {
DefaultToolbarAction( toolbarActions.add(
label = stringResource(R.string.menu_app_info), DefaultToolbarAction(
icon = Icons.Rounded.Info label = stringResource(R.string.menu_app_info),
) { icon = Icons.Rounded.Info
app.openAppDetails(context) ) {
}) app.openAppDetails(context)
})
}
toolbarActions.add( toolbarActions.add(
DefaultToolbarAction( DefaultToolbarAction(
@ -392,49 +403,56 @@ fun AppItem(
) )
val sheetManager = LocalBottomSheetManager.current val sheetManager = LocalBottomSheetManager.current
toolbarActions.add(DefaultToolbarAction( if (!app.isPrivate) {
label = stringResource(R.string.menu_customize), toolbarActions.add(DefaultToolbarAction(
icon = Icons.Rounded.Tune, label = stringResource(R.string.menu_customize),
action = { sheetManager.showCustomizeSearchableModal(app) } icon = Icons.Rounded.Tune,
)) action = { sheetManager.showCustomizeSearchableModal(app) }
))
val storeDetails = remember(app) { app.getStoreDetails(context) } }
val shareAction = if (storeDetails == null) {
DefaultToolbarAction( if (!app.isPrivate) {
label = stringResource(R.string.menu_share), val storeDetails = remember(app) { app.getStoreDetails(context) }
icon = Icons.Rounded.Share val shareAction = if (storeDetails == null) {
) { DefaultToolbarAction(
scope.launch { label = stringResource(R.string.menu_share),
app.shareApkFile(context) icon = Icons.Rounded.Share
} ) {
} scope.launch {
} else { app.shareApkFile(context)
SubmenuToolbarAction( }
label = stringResource(R.string.menu_share), }
icon = Icons.Rounded.Share, } else {
children = listOf( SubmenuToolbarAction(
DefaultToolbarAction( label = stringResource(R.string.menu_share),
label = stringResource(R.string.menu_share_store_link, storeDetails.label), icon = Icons.Rounded.Share,
icon = Icons.Rounded.Link, children = listOf(
action = { DefaultToolbarAction(
val shareIntent = Intent(Intent.ACTION_SEND) label = stringResource(
shareIntent.putExtra(Intent.EXTRA_TEXT, storeDetails.url) R.string.menu_share_store_link,
shareIntent.type = "text/plain" storeDetails.label
context.startActivity(Intent.createChooser(shareIntent, null)) ),
} icon = Icons.Rounded.Link,
), action = {
DefaultToolbarAction( val shareIntent = Intent(Intent.ACTION_SEND)
label = stringResource(R.string.menu_share_apk_file), shareIntent.putExtra(Intent.EXTRA_TEXT, storeDetails.url)
icon = Icons.Rounded.Android shareIntent.type = "text/plain"
) { context.startActivity(Intent.createChooser(shareIntent, null))
scope.launch { }
app.shareApkFile(context) ),
} DefaultToolbarAction(
} label = stringResource(R.string.menu_share_apk_file),
) icon = Icons.Rounded.Android
) ) {
scope.launch {
app.shareApkFile(context)
}
}
)
)
}
toolbarActions.add(shareAction)
} }
toolbarActions.add(shareAction)
if (app.canUninstall) { if (app.canUninstall) {
toolbarActions.add( toolbarActions.add(

View File

@ -17,8 +17,13 @@ interface Application: SavableSearchable {
get() = false get() = false
val componentName: ComponentName val componentName: ComponentName
val isSystemApp: Boolean
val isSuspended: Boolean val isSuspended: Boolean
/**
* If true, the app's identity should not be revealed to the user.
*/
val isPrivate: Boolean
get() = false
val user: UserHandle val user: UserHandle
val versionName: String? val versionName: String?

View File

@ -806,6 +806,7 @@
<string name="import_theme_apply">Apply theme</string> <string name="import_theme_apply">Apply theme</string>
<string name="import_theme_error">The selected file could not be read. Please make sure that you selected a valid theme file (*.kvtheme), and that the file is not corrupt.</string> <string name="import_theme_error">The selected file could not be read. Please make sure that you selected a valid theme file (*.kvtheme), and that the file is not corrupt.</string>
<string name="shortcut_label_unavailable">Unavailable</string> <string name="shortcut_label_unavailable">Unavailable</string>
<string name="app_label_locked_profile">Locked</string>
<!-- %1$s: app name --> <!-- %1$s: app name -->
<string name="shortcut_unavailable_description">This shortcut is unavailable because %1$s isn\'t the default launcher</string> <string name="shortcut_unavailable_description">This shortcut is unavailable because %1$s isn\'t the default launcher</string>
<string name="preference_plugin_enable">Enable plugin</string> <string name="preference_plugin_enable">Enable plugin</string>

View File

@ -37,6 +37,7 @@ dependencies {
implementation(libs.bundles.kotlin) implementation(libs.bundles.kotlin)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.materialicons)
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)

View File

@ -12,7 +12,6 @@ import de.mm20.launcher2.search.SearchableSerializer
class FakeApp: Application { class FakeApp: Application {
override val componentName: ComponentName = ComponentName(randomString(), randomString()) override val componentName: ComponentName = ComponentName(randomString(), randomString())
override val isSystemApp: Boolean = false
override val isSuspended: Boolean = false override val isSuspended: Boolean = false
override val user: UserHandle = Process.myUserHandle() override val user: UserHandle = Process.myUserHandle()
override val versionName: String = "1.0" override val versionName: String = "1.0"

View File

@ -7,11 +7,26 @@ import android.content.pm.LauncherApps
import android.os.UserManager import android.os.UserManager
import android.util.Log import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import org.json.JSONObject import org.json.JSONObject
internal class LockedPrivateProfileAppSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as LockedPrivateProfileApp
val json = JSONObject()
json.put("package", searchable.componentName.packageName)
json.put("activity", searchable.componentName.className)
json.put("user", searchable.userSerialNumber)
return json.toString()
}
override val typePrefix: String
get() = "app"
}
class LauncherAppSerializer : SearchableSerializer { class LauncherAppSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as LauncherApp searchable as LauncherApp
@ -33,9 +48,26 @@ class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
val userManager = context.getSystemService<UserManager>()!! val userManager = context.getSystemService<UserManager>()!!
val userSerial = json.optLong("user") val userSerial = json.optLong("user")
val user = userManager.getUserForSerialNumber(userSerial) ?: return null val user = userManager.getUserForSerialNumber(userSerial) ?: return null
val pkg = json.getString("package") val pkg = json.getString("package")
val activity = json.getString("activity")
val componentName = ComponentName(pkg, activity)
if (isAtLeastApiLevel(35)) {
val launcherUser = launcherApps.getLauncherUserInfo(user) ?: return null
if (launcherUser.userType == UserManager.USER_TYPE_PROFILE_PRIVATE && userManager.isQuietModeEnabled(user)) {
return LockedPrivateProfileApp(
label = context.getString(R.string.app_label_locked_profile),
componentName = componentName,
user = user,
userSerialNumber = userSerial
)
}
}
val intent = Intent().also { val intent = Intent().also {
it.component = ComponentName(pkg, json.getString("activity")) it.component = componentName
} }
try { try {
val launcherActivityInfo = launcherApps.resolveActivity(intent, user) ?: return null val launcherActivityInfo = launcherApps.resolveActivity(intent, user) ?: return null

View File

@ -60,7 +60,7 @@ internal data class LauncherApp(
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle() private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
override val isSystemApp: Boolean = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0 private val isSystemApp: Boolean = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
override val canUninstall: Boolean override val canUninstall: Boolean
get() = !isSystemApp && isMainProfile get() = !isSystemApp && isMainProfile

View File

@ -0,0 +1,103 @@
package de.mm20.launcher2.applications
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherApps
import android.os.Bundle
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Lock
import androidx.core.content.getSystemService
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.VectorLayer
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableSerializer
internal data class LockedPrivateProfileApp(
override val label: String,
override val componentName: ComponentName,
override val user: UserHandle,
internal val userSerialNumber: Long,
): Application {
override val isSuspended: Boolean = false
override val versionName: String? = null
override val canUninstall: Boolean = false
override val isPrivate: Boolean = true
override fun uninstall(context: Context) {
// Do nothing
}
override fun openAppDetails(context: Context) {
// Do nothing
}
override val domain: String = LauncherApp.Domain
override val canShareApk: Boolean = false
override val key: String = "${domain}://${componentName.packageName}:${componentName.className}:${userSerialNumber}"
override fun overrideLabel(label: String): SavableSearchable {
// We don't expose custom labels for locked apps
return this
}
override fun launch(context: Context, options: Bundle?): Boolean {
if (!isAtLeastApiLevel(35)) return false
val userManager = context.getSystemService<UserManager>() ?: return false
if (userManager.isQuietModeEnabled(user)) {
userManager.requestQuietModeEnabled(false, user)
return true
}
val launcherApps = context.getSystemService<LauncherApps>() ?: return false
if (isAtLeastApiLevel(31)) {
options?.putInt("android.activity.splashScreenStyle", 1)
}
try {
launcherApps.startMainActivity(
componentName,
user,
null,
options
)
} catch (e: SecurityException) {
Log.e("MM20", "Could not launch app", e)
return false
} catch (e: ActivityNotFoundException) {
Log.e("MM20", "Could not launch app", e)
return false
}
return true
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = VectorLayer(
vector = Icons.Rounded.Lock,
),
backgroundLayer = ColorLayer(0)
)
}
override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? {
return null
}
override fun getSerializer(): SearchableSerializer {
return LockedPrivateProfileAppSerializer()
}
}

View File

@ -46,6 +46,9 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class IconService( class IconService(
@ -140,6 +143,12 @@ class IconService(
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> { fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {
if (searchable is Application && searchable.isPrivate) {
return transformations.map {
searchable.getPlaceholderIcon(context).transform(it)
}
}
val customIcon = customAttributesRepository.getCustomIcon(searchable) val customIcon = customAttributesRepository.getCustomIcon(searchable)
return combine(iconProviders, transformations, customIcon) { providers, transformations, ci -> return combine(iconProviders, transformations, customIcon) { providers, transformations, ci ->
var icon = cache.get(searchable.key + ci.hashCode() + providers.hashCode() + transformations.hashCode()) var icon = cache.get(searchable.key + ci.hashCode() + providers.hashCode() + transformations.hashCode())