React to changing profile availability

This commit is contained in:
MM20 2024-07-07 14:29:42 +02:00
parent 84f3d9f825
commit 5a28eb584e
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
29 changed files with 424 additions and 77 deletions

View File

@ -152,6 +152,7 @@ dependencies {
implementation(project(":data:notifications"))
implementation(project(":libs:owncloud"))
implementation(project(":core:permissions"))
implementation(project(":core:profiles"))
implementation(project(":core:preferences"))
implementation(project(":services:search"))
implementation(project(":services:tags"))

View File

@ -32,6 +32,7 @@ import de.mm20.launcher2.data.plugins.dataPluginsModule
import de.mm20.launcher2.devicepose.devicePoseModule
import de.mm20.launcher2.plugins.servicesPluginsModule
import de.mm20.launcher2.preferences.preferencesModule
import de.mm20.launcher2.profiles.profilesModule
import de.mm20.launcher2.searchactions.searchActionsModule
import de.mm20.launcher2.services.favorites.favoritesModule
import de.mm20.launcher2.services.tags.servicesTagsModule
@ -94,6 +95,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
servicesPluginsModule,
backupModule,
devicePoseModule,
profilesModule,
)
)
}

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.launcher.search
import android.content.Context
import android.os.Process
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
@ -16,7 +17,6 @@ import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.preferences.search.SearchFilterSettings
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
import de.mm20.launcher2.preferences.ui.SearchUiSettings
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Article
@ -268,7 +268,7 @@ class SearchVM : ViewModel(), KoinComponent {
r is SavableSearchable && hiddenKeys.contains(r.key) && !filters.hiddenItems -> {
hidden.add(r)
}
r is Application && r.profile == AppProfile.Work -> workApps.add(r)
r is Application && r.user != Process.myUserHandle() -> workApps.add(r)
r is Application -> apps.add(r)
r is AppShortcut -> shortcuts.add(r)
r is File -> files.add(r)

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.settings.media
import android.os.Process
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
@ -12,7 +13,6 @@ import de.mm20.launcher2.music.MusicService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.media.MediaSettings
import de.mm20.launcher2.search.AppProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@ -51,7 +51,7 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
loading.value = true
viewModelScope.launch(Dispatchers.Default) {
val musicApps = musicService.getInstalledPlayerPackages()
val allApps = appRepository.findMany().first { it.isNotEmpty() }.filter { it.profile == AppProfile.Personal }
val allApps = appRepository.findMany().first { it.isNotEmpty() }.filter { it.user == Process.myUserHandle() }
.distinctBy { it.componentName.packageName }
val settings = mediaSettings.first()
val allowList = settings.allowList

View File

@ -0,0 +1,54 @@
package de.mm20.launcher2.profiles
import android.content.Context
import android.os.Process
import android.os.UserHandle
import de.mm20.launcher2.ktx.getSerialNumber
data class Profile(
val type: Profile.Type,
val userHandle: UserHandle,
val serial: Long,
) {
override fun equals(other: Any?): Boolean {
if (other is Profile) {
return userHandle == other.userHandle
}
return super.equals(other)
}
override fun hashCode(): Int {
return userHandle.hashCode()
}
enum class Type {
/**
* The default profile.
*/
Personal,
/**
* The work profile.
*/
Work,
/**
* The private space profile (Android 15+)
*/
Private,
}
data class State(
val locked: Boolean = false,
val hidden: Boolean = false,
)
companion object {
fun fromContext(context: Context): Profile {
val userHandle = Process.myUserHandle()
val serial = userHandle.getSerialNumber(context)
return Profile(Profile.Type.Personal, userHandle, serial)
}
}
}

View File

@ -9,6 +9,7 @@ import de.mm20.launcher2.base.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.profiles.Profile
interface AppShortcut : SavableSearchable {
@ -39,6 +40,4 @@ interface AppShortcut : SavableSearchable {
val isUnavailable: Boolean
get() = false
val profile: AppProfile
}

View File

@ -10,11 +10,7 @@ import de.mm20.launcher2.base.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
enum class AppProfile {
Personal,
Work,
}
import de.mm20.launcher2.profiles.Profile
interface Application: SavableSearchable {
override val preferDetailsOverLaunch: Boolean
@ -23,7 +19,6 @@ interface Application: SavableSearchable {
val componentName: ComponentName
val isSystemApp: Boolean
val isSuspended: Boolean
val profile: AppProfile
val user: UserHandle
val versionName: String?

View File

@ -65,6 +65,7 @@ enum class PermissionGroup {
Notifications,
AppShortcuts,
Accessibility,
HiddenProfiles,
}
internal class PermissionsManagerImpl(
@ -90,6 +91,9 @@ internal class PermissionsManagerImpl(
private val appShortcutsPermissionState = MutableStateFlow(
checkPermissionOnce(PermissionGroup.AppShortcuts)
)
private val hiddenProfilesPermissionState = MutableStateFlow(
checkPermissionOnce(PermissionGroup.HiddenProfiles)
)
override fun requestPermission(context: AppCompatActivity, permissionGroup: PermissionGroup) {
when (permissionGroup) {
@ -142,6 +146,7 @@ internal class PermissionsManagerImpl(
}
}
PermissionGroup.HiddenProfiles,
PermissionGroup.AppShortcuts -> {
if (isAtLeastApiLevel(29)) {
val roleManager = context.getSystemService<RoleManager>()
@ -196,6 +201,12 @@ internal class PermissionsManagerImpl(
context.getSystemService<LauncherApps>()?.hasShortcutHostPermission() == true
}
PermissionGroup.HiddenProfiles -> {
if (isAtLeastApiLevel(29)) {
context.getSystemService<RoleManager>()?.isRoleHeld(RoleManager.ROLE_HOME) == true
} else false
}
PermissionGroup.Accessibility -> {
accessibilityPermissionState.value
}
@ -211,6 +222,7 @@ internal class PermissionsManagerImpl(
PermissionGroup.Notifications -> notificationsPermissionState
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
PermissionGroup.Accessibility -> accessibilityPermissionState
PermissionGroup.HiddenProfiles -> hiddenProfilesPermissionState
}
}
@ -229,12 +241,14 @@ internal class PermissionsManagerImpl(
PermissionGroup.Notifications -> notificationsPermissionState.value = granted
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
PermissionGroup.HiddenProfiles -> hiddenProfilesPermissionState.value = granted
}
}
override fun onResume() {
externalStoragePermissionState.value = checkPermissionOnce(PermissionGroup.ExternalStorage)
appShortcutsPermissionState.value = checkPermissionOnce(PermissionGroup.AppShortcuts)
hiddenProfilesPermissionState.value = checkPermissionOnce(PermissionGroup.HiddenProfiles)
}
override fun reportNotificationListenerState(running: Boolean) {

1
core/profiles/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,50 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
}
android {
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
create("nightly") {
initWith(getByName("release"))
matchingFallbacks += "release"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.profiles"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.koin.android)
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":core:permissions"))
}

View File

21
core/profiles/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.profiles
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val profilesModule = module {
single<ProfileManager> { ProfileManager(androidContext(), get()) }
}

View File

@ -0,0 +1,168 @@
package de.mm20.launcher2.profiles
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.LauncherApps
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal data class ProfileWithState(
val profile: Profile,
val state: Profile.State,
)
class ProfileManager(
private val context: Context,
private val permissionsManager: PermissionsManager,
) {
private val userManager = context.getSystemService<UserManager>()!!
private val launcherApps = context.getSystemService<LauncherApps>()!!
private val scope = CoroutineScope(Dispatchers.Default + Job())
private val profileStates: MutableStateFlow<List<ProfileWithState>> =
MutableStateFlow(emptyList())
/**
* List of profiles that are active and unlocked.
*/
val activeProfiles: Flow<List<Profile>> = profileStates.map {
it.mapNotNull {
if (it.state.hidden) null else it.profile
}
}.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
init {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
scope.launch {
refreshProfiles()
}
}
}
context.registerReceiver(
receiver, IntentFilter().apply {
addAction(Intent.ACTION_MANAGED_PROFILE_ADDED)
addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)
addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)
if (isAtLeastApiLevel(34)) {
addAction(Intent.ACTION_PROFILE_ADDED)
addAction(Intent.ACTION_PROFILE_REMOVED)
}
if (isAtLeastApiLevel(31)) {
addAction(Intent.ACTION_PROFILE_ACCESSIBLE)
addAction(Intent.ACTION_PROFILE_INACCESSIBLE)
}
}
)
scope.launch {
if (isAtLeastApiLevel(35)) {
permissionsManager.hasPermission(PermissionGroup.HiddenProfiles).collectLatest {
refreshProfiles()
}
} else {
refreshProfiles()
}
}
}
private val mutex = Mutex()
private suspend fun refreshProfiles() {
mutex.withLock {
val profiles = mutableListOf<ProfileWithState>()
for (userHandle in launcherApps.profiles) {
profiles.add(
ProfileWithState(
Profile(
type = getProfileType(userHandle),
userHandle = userHandle,
serial = userManager.getSerialNumberForUser(userHandle),
),
getProfileState(userHandle),
)
)
}
profileStates.value = profiles
}
}
fun getProfile(userHandle: UserHandle): Flow<Profile?> {
return profileStates.map {
it.find { it.profile.userHandle == userHandle }?.profile
}
}
fun getProfileState(profile: Profile): Flow<Profile.State?> {
return profileStates.map { profiles ->
profiles.find { it.profile == profile }?.state
}
}
/**
* This only works when the launcher is installed in the primary profile.
*/
private fun getProfileType(userHandle: UserHandle): Profile.Type {
return when {
userManager.isManagedProfile(userHandle) -> Profile.Type.Work
userHandle == Process.myUserHandle() -> Profile.Type.Personal
else -> Profile.Type.Private
}
}
private fun getProfileState(userHandle: UserHandle): Profile.State {
return Profile.State(
locked = !userManager.isUserUnlocked(userHandle),
hidden = !userManager.isUserUnlocked(userHandle),
)
}
@RequiresApi(28)
fun unlockProfile(profile: Profile) {
userManager.requestQuietModeEnabled(false, profile.userHandle)
}
@RequiresApi(28)
fun lockProfile(profile: Profile) {
userManager.requestQuietModeEnabled(true, profile.userHandle)
}
}
internal fun UserManager.isManagedProfile(userHandle: UserHandle): Boolean {
try {
val isManagedProfile = UserManager::class.java.getDeclaredMethod(
"isManagedProfile",
Int::class.javaPrimitiveType
)
val serial = getSerialNumberForUser(userHandle).toInt()
return isManagedProfile.invoke(this, serial) as Boolean
} catch (e: Exception) {
Log.e("MM20", "isManagedProfile could not be invoked", e)
return false
}
}

View File

@ -47,5 +47,6 @@ dependencies {
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":core:compat"))
implementation(project(":core:profiles"))
}

View File

@ -5,7 +5,6 @@ import android.content.Context
import android.os.Bundle
import android.os.Process
import android.os.UserHandle
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.NullSerializer
import de.mm20.launcher2.search.SavableSearchable
@ -15,7 +14,6 @@ class FakeApp: Application {
override val componentName: ComponentName = ComponentName(randomString(), randomString())
override val isSystemApp: Boolean = false
override val isSuspended: Boolean = false
override val profile: AppProfile = AppProfile.Personal
override val user: UserHandle = Process.myUserHandle()
override val versionName: String = "1.0"
override val canUninstall: Boolean = false

View File

@ -11,6 +11,8 @@ import android.os.Looper
import android.os.Process
import android.os.UserHandle
import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.profiles.Profile
import de.mm20.launcher2.profiles.ProfileManager
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList
@ -20,7 +22,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -38,6 +42,7 @@ interface AppRepository : SearchableRepository<Application> {
internal class AppRepositoryImpl(
private val context: Context,
private val profileManager: ProfileManager,
) : AppRepository {
private val scope = CoroutineScope(Dispatchers.Default + Job())
@ -45,11 +50,8 @@ internal class AppRepositoryImpl(
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
private val installedApps = MutableStateFlow<List<LauncherApp>>(emptyList())
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
private val profiles: List<UserHandle>
get() = launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
private val profiles = profileManager.activeProfiles
private val mutex = Mutex()
@ -163,21 +165,42 @@ internal class AppRepositoryImpl(
}
}, Handler(Looper.getMainLooper()))
scope.launch {
profiles.runningFold<List<Profile>, Pair<List<Profile>?, List<Profile>?>>(null to null) { acc, value ->
acc.second to value
}.collectLatest { (prev, curr) ->
if (curr == null) return@collectLatest
if (prev == null) {
curr.forEach { addProfile(it) }
} else {
val added = curr - prev
val removed = prev - curr
added.forEach { addProfile(it) }
removed.forEach { removeProfile(it) }
}
}
}
}
private suspend fun addProfile(profile: Profile) {
mutex.withLock {
val apps = installedApps.value.toMutableList()
apps.addAll(getApplications(null, profile.userHandle))
installedApps.value = apps
}
}
private fun removeProfile(profile: Profile) {
scope.launch {
mutex.withLock {
val apps = profiles.map { p ->
try {
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it) }
} catch (e: SecurityException) {
emptyList()
}
}.flatten()
val apps = installedApps.value.toMutableList()
apps.removeAll { it.user == profile.userHandle }
installedApps.value = apps
}
}
}
private fun getApplications(packageName: String, userHandle: UserHandle): List<LauncherApp> {
private fun getApplications(packageName: String?, userHandle: UserHandle): List<LauncherApp> {
if (packageName == context.packageName) return emptyList()
return try {

View File

@ -27,7 +27,6 @@ import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.icons.TransparentLayer
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.StoreLink
@ -61,9 +60,6 @@ internal data class LauncherApp(
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
override val profile: AppProfile
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
override val isSystemApp: Boolean = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
override val canUninstall: Boolean

View File

@ -8,7 +8,7 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module
val applicationsModule = module {
factory<SearchableRepository<Application>>(named<Application>()) { AppRepositoryImpl(androidContext()) }
factory<AppRepository> { AppRepositoryImpl(androidContext()) }
factory<SearchableRepository<Application>>(named<Application>()) { get<AppRepository>() }
single<AppRepository> { AppRepositoryImpl(androidContext(), get()) }
factory<SearchableDeserializer>(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) }
}

View File

@ -5,10 +5,8 @@ import android.content.IntentSender
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.graphics.drawable.Drawable
import android.os.Process
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.romanize
import de.mm20.launcher2.search.AppProfile
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.text.Collator
@ -17,7 +15,6 @@ class AppShortcutConfigActivity(
private val launcherActivityInfo: LauncherActivityInfo,
): Comparable<AppShortcutConfigActivity> {
val label = launcherActivityInfo.label.toString()
val profile: AppProfile = if (launcherActivityInfo.user == Process.myUserHandle()) AppProfile.Personal else AppProfile.Work
fun getIcon(context: Context): Flow<Drawable?> = flow {
val icon = launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi)

View File

@ -18,7 +18,6 @@ import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.coroutines.Dispatchers
@ -69,9 +68,6 @@ internal data class LauncherShortcut(
override val preferDetailsOverLaunch: Boolean = false
private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
override val profile: AppProfile
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
override val key: String
get() = if (isMainProfile) {

View File

@ -11,7 +11,6 @@ import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SearchableSerializer
@ -26,9 +25,6 @@ internal data class LegacyShortcut(
override val domain = Domain
override val key: String = "$domain://${intent.toUri(0)}"
override val profile: AppProfile
get() = AppProfile.Personal
override fun overrideLabel(label: String): LegacyShortcut {
return this.copy(labelOverride = label)
}

View File

@ -6,11 +6,9 @@ import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableSerializer
@ -67,8 +65,6 @@ internal class UnavailableShortcut(
}
override val isUnavailable: Boolean = true
override val profile: AppProfile
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
companion object {
internal operator fun invoke(context: Context, id: String, packageName: String, user: UserHandle, userSerial: Long): UnavailableShortcut? {

View File

@ -48,6 +48,7 @@ dependencies {
implementation(project(":data:appshortcuts"))
implementation(project(":data:notifications"))
implementation(project(":core:preferences"))
implementation(project(":core:profiles"))
implementation(project(":core:base"))
implementation(project(":data:files"))
implementation(project(":data:searchable"))

View File

@ -8,7 +8,7 @@ import de.mm20.launcher2.badges.providers.HiddenItemBadgeProvider
import de.mm20.launcher2.badges.providers.NotificationBadgeProvider
import de.mm20.launcher2.badges.providers.PluginBadgeProvider
import de.mm20.launcher2.badges.providers.SuspendedAppsBadgeProvider
import de.mm20.launcher2.badges.providers.WorkProfileBadgeProvider
import de.mm20.launcher2.badges.providers.ProfileBadgeProvider
import de.mm20.launcher2.preferences.ui.BadgeSettings
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.CoroutineScope
@ -42,7 +42,7 @@ internal class BadgeServiceImpl(
scope.launch {
settings.distinctUntilChanged().collectLatest {
val providers = mutableListOf<BadgeProvider>()
providers += WorkProfileBadgeProvider()
providers += ProfileBadgeProvider()
providers += HiddenItemBadgeProvider()
if (it.notifications) {
providers += NotificationBadgeProvider()

View File

@ -0,0 +1,53 @@
package de.mm20.launcher2.badges.providers
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.Work
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeIcon
import de.mm20.launcher2.profiles.Profile
import de.mm20.launcher2.profiles.ProfileManager
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ProfileBadgeProvider : BadgeProvider, KoinComponent {
private val profileManager: ProfileManager by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = flow {
val userHandle = when(searchable) {
is Application -> searchable.user
is AppShortcut -> searchable.user
else -> null
}
if (userHandle != null) {
emitAll(
profileManager.getProfile(userHandle).map {
when(it?.type) {
Profile.Type.Work -> WorkProfile
Profile.Type.Private -> PrivateProfile
else -> null
}
}
)
} else {
emit(null)
}
}
companion object {
private val WorkProfile = Badge(
icon = BadgeIcon(Icons.Rounded.Work)
)
private val PrivateProfile = Badge(
icon = BadgeIcon(Icons.Rounded.Lock)
)
}
}

View File

@ -1,28 +0,0 @@
package de.mm20.launcher2.badges.providers
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Work
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeIcon
import de.mm20.launcher2.badges.MutableBadge
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class WorkProfileBadgeProvider : BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> = flow {
if (searchable is Application && searchable.profile == AppProfile.Work || searchable is AppShortcut && searchable.profile == AppProfile.Work) {
emit(
MutableBadge(
icon = BadgeIcon(Icons.Rounded.Work)
)
)
} else {
emit(null)
}
}
}

View File

@ -68,3 +68,4 @@ include(":plugins:sdk")
include(":data:locations")
include(":services:plugins")
include(":core:devicepose")
include(":core:profiles")