From 318c26a4396eecb415de24910aa18963237a76fc Mon Sep 17 00:00:00 2001
From: MM20 <15646950+MM2-0@users.noreply.github.com>
Date: Tue, 13 Aug 2024 15:19:24 +0200
Subject: [PATCH] Mask private profile apps while private space is locked
---
.../ui/launcher/search/apps/AppItem.kt | 158 ++++++++++--------
.../de/mm20/launcher2/search/Application.kt | 7 +-
core/i18n/src/main/res/values/strings.xml | 1 +
data/applications/build.gradle.kts | 1 +
.../de/mm20/launcher2/applications/FakeApp.kt | 1 -
.../applications/AppSerialization.kt | 34 +++-
.../launcher2/applications/LauncherApp.kt | 2 +-
.../applications/LockedPrivateProfileApp.kt | 103 ++++++++++++
.../de/mm20/launcher2/icons/IconService.kt | 9 +
9 files changed, 242 insertions(+), 74 deletions(-)
create mode 100644 data/applications/src/main/java/de/mm20/launcher2/applications/LockedPrivateProfileApp.kt
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt
index 2a9390be..0cbb40db 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt
@@ -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(
diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Application.kt b/core/base/src/main/java/de/mm20/launcher2/search/Application.kt
index 4a36d9e0..0efda7c4 100644
--- a/core/base/src/main/java/de/mm20/launcher2/search/Application.kt
+++ b/core/base/src/main/java/de/mm20/launcher2/search/Application.kt
@@ -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?
diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml
index a11efd46..59c2f4ae 100644
--- a/core/i18n/src/main/res/values/strings.xml
+++ b/core/i18n/src/main/res/values/strings.xml
@@ -806,6 +806,7 @@
Apply theme
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.
Unavailable
+ Locked
This shortcut is unavailable because %1$s isn\'t the default launcher
Enable plugin
diff --git a/data/applications/build.gradle.kts b/data/applications/build.gradle.kts
index ca47f64b..4914e8ab 100644
--- a/data/applications/build.gradle.kts
+++ b/data/applications/build.gradle.kts
@@ -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)
diff --git a/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt
index d5392ef5..8e1a0653 100644
--- a/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt
+++ b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt
@@ -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"
diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/AppSerialization.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/AppSerialization.kt
index 3907ff26..c4a8b938 100644
--- a/data/applications/src/main/java/de/mm20/launcher2/applications/AppSerialization.kt
+++ b/data/applications/src/main/java/de/mm20/launcher2/applications/AppSerialization.kt
@@ -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()!!
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
diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt
index 28bd72c7..d52ddb25 100644
--- a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt
+++ b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt
@@ -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
diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/LockedPrivateProfileApp.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/LockedPrivateProfileApp.kt
new file mode 100644
index 00000000..028ce215
--- /dev/null
+++ b/data/applications/src/main/java/de/mm20/launcher2/applications/LockedPrivateProfileApp.kt
@@ -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() ?: return false
+
+ if (userManager.isQuietModeEnabled(user)) {
+ userManager.requestQuietModeEnabled(false, user)
+ return true
+ }
+
+ val launcherApps = context.getSystemService() ?: 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()
+ }
+}
\ No newline at end of file
diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
index e470cc1f..b8d24da4 100644
--- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
+++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
@@ -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 {
+ 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())