Mask private profile apps while private space is locked
This commit is contained in:
parent
74060d53c3
commit
318c26a439
@ -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(
|
||||||
|
|||||||
@ -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?
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user