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

View File

@ -17,8 +17,13 @@ interface Application: SavableSearchable {
get() = false
val componentName: ComponentName
val isSystemApp: 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 versionName: String?

View File

@ -806,6 +806,7 @@
<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="shortcut_label_unavailable">Unavailable</string>
<string name="app_label_locked_profile">Locked</string>
<!-- %1$s: app name -->
<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>

View File

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

View File

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

View File

@ -7,11 +7,26 @@ import android.content.pm.LauncherApps
import android.os.UserManager
import android.util.Log
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
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 {
override fun serialize(searchable: SavableSearchable): String {
searchable as LauncherApp
@ -33,9 +48,26 @@ class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
val userManager = context.getSystemService<UserManager>()!!
val userSerial = json.optLong("user")
val user = userManager.getUserForSerialNumber(userSerial) ?: return null
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 {
it.component = ComponentName(pkg, json.getString("activity"))
it.component = componentName
}
try {
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()
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
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.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class IconService(
@ -140,6 +143,12 @@ class IconService(
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)
return combine(iconProviders, transformations, customIcon) { providers, transformations, ci ->
var icon = cache.get(searchable.key + ci.hashCode() + providers.hashCode() + transformations.hashCode())