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
|
||||
)
|
||||
|
||||
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(
|
||||
|
||||
@ -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?
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.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())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user