React to changing profile availability
This commit is contained in:
parent
84f3d9f825
commit
5a28eb584e
@ -152,6 +152,7 @@ dependencies {
|
|||||||
implementation(project(":data:notifications"))
|
implementation(project(":data:notifications"))
|
||||||
implementation(project(":libs:owncloud"))
|
implementation(project(":libs:owncloud"))
|
||||||
implementation(project(":core:permissions"))
|
implementation(project(":core:permissions"))
|
||||||
|
implementation(project(":core:profiles"))
|
||||||
implementation(project(":core:preferences"))
|
implementation(project(":core:preferences"))
|
||||||
implementation(project(":services:search"))
|
implementation(project(":services:search"))
|
||||||
implementation(project(":services:tags"))
|
implementation(project(":services:tags"))
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import de.mm20.launcher2.data.plugins.dataPluginsModule
|
|||||||
import de.mm20.launcher2.devicepose.devicePoseModule
|
import de.mm20.launcher2.devicepose.devicePoseModule
|
||||||
import de.mm20.launcher2.plugins.servicesPluginsModule
|
import de.mm20.launcher2.plugins.servicesPluginsModule
|
||||||
import de.mm20.launcher2.preferences.preferencesModule
|
import de.mm20.launcher2.preferences.preferencesModule
|
||||||
|
import de.mm20.launcher2.profiles.profilesModule
|
||||||
import de.mm20.launcher2.searchactions.searchActionsModule
|
import de.mm20.launcher2.searchactions.searchActionsModule
|
||||||
import de.mm20.launcher2.services.favorites.favoritesModule
|
import de.mm20.launcher2.services.favorites.favoritesModule
|
||||||
import de.mm20.launcher2.services.tags.servicesTagsModule
|
import de.mm20.launcher2.services.tags.servicesTagsModule
|
||||||
@ -94,6 +95,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
|
|||||||
servicesPluginsModule,
|
servicesPluginsModule,
|
||||||
backupModule,
|
backupModule,
|
||||||
devicePoseModule,
|
devicePoseModule,
|
||||||
|
profilesModule,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Process
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
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.SearchFilterSettings
|
||||||
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
||||||
import de.mm20.launcher2.preferences.ui.SearchUiSettings
|
import de.mm20.launcher2.preferences.ui.SearchUiSettings
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import de.mm20.launcher2.search.AppShortcut
|
import de.mm20.launcher2.search.AppShortcut
|
||||||
import de.mm20.launcher2.search.Application
|
import de.mm20.launcher2.search.Application
|
||||||
import de.mm20.launcher2.search.Article
|
import de.mm20.launcher2.search.Article
|
||||||
@ -268,7 +268,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
r is SavableSearchable && hiddenKeys.contains(r.key) && !filters.hiddenItems -> {
|
r is SavableSearchable && hiddenKeys.contains(r.key) && !filters.hiddenItems -> {
|
||||||
hidden.add(r)
|
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 Application -> apps.add(r)
|
||||||
r is AppShortcut -> shortcuts.add(r)
|
r is AppShortcut -> shortcuts.add(r)
|
||||||
r is File -> files.add(r)
|
r is File -> files.add(r)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.mm20.launcher2.ui.settings.media
|
package de.mm20.launcher2.ui.settings.media
|
||||||
|
|
||||||
|
import android.os.Process
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
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.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.media.MediaSettings
|
import de.mm20.launcher2.preferences.media.MediaSettings
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@ -51,7 +51,7 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
val musicApps = musicService.getInstalledPlayerPackages()
|
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 }
|
.distinctBy { it.componentName.packageName }
|
||||||
val settings = mediaSettings.first()
|
val settings = mediaSettings.first()
|
||||||
val allowList = settings.allowList
|
val allowList = settings.allowList
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import de.mm20.launcher2.base.R
|
|||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
import de.mm20.launcher2.icons.TintedIconLayer
|
||||||
|
import de.mm20.launcher2.profiles.Profile
|
||||||
|
|
||||||
interface AppShortcut : SavableSearchable {
|
interface AppShortcut : SavableSearchable {
|
||||||
|
|
||||||
@ -39,6 +40,4 @@ interface AppShortcut : SavableSearchable {
|
|||||||
|
|
||||||
val isUnavailable: Boolean
|
val isUnavailable: Boolean
|
||||||
get() = false
|
get() = false
|
||||||
|
|
||||||
val profile: AppProfile
|
|
||||||
}
|
}
|
||||||
@ -10,11 +10,7 @@ import de.mm20.launcher2.base.R
|
|||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
import de.mm20.launcher2.icons.TintedIconLayer
|
||||||
|
import de.mm20.launcher2.profiles.Profile
|
||||||
enum class AppProfile {
|
|
||||||
Personal,
|
|
||||||
Work,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Application: SavableSearchable {
|
interface Application: SavableSearchable {
|
||||||
override val preferDetailsOverLaunch: Boolean
|
override val preferDetailsOverLaunch: Boolean
|
||||||
@ -23,7 +19,6 @@ interface Application: SavableSearchable {
|
|||||||
val componentName: ComponentName
|
val componentName: ComponentName
|
||||||
val isSystemApp: Boolean
|
val isSystemApp: Boolean
|
||||||
val isSuspended: Boolean
|
val isSuspended: Boolean
|
||||||
val profile: AppProfile
|
|
||||||
val user: UserHandle
|
val user: UserHandle
|
||||||
val versionName: String?
|
val versionName: String?
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,7 @@ enum class PermissionGroup {
|
|||||||
Notifications,
|
Notifications,
|
||||||
AppShortcuts,
|
AppShortcuts,
|
||||||
Accessibility,
|
Accessibility,
|
||||||
|
HiddenProfiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PermissionsManagerImpl(
|
internal class PermissionsManagerImpl(
|
||||||
@ -90,6 +91,9 @@ internal class PermissionsManagerImpl(
|
|||||||
private val appShortcutsPermissionState = MutableStateFlow(
|
private val appShortcutsPermissionState = MutableStateFlow(
|
||||||
checkPermissionOnce(PermissionGroup.AppShortcuts)
|
checkPermissionOnce(PermissionGroup.AppShortcuts)
|
||||||
)
|
)
|
||||||
|
private val hiddenProfilesPermissionState = MutableStateFlow(
|
||||||
|
checkPermissionOnce(PermissionGroup.HiddenProfiles)
|
||||||
|
)
|
||||||
|
|
||||||
override fun requestPermission(context: AppCompatActivity, permissionGroup: PermissionGroup) {
|
override fun requestPermission(context: AppCompatActivity, permissionGroup: PermissionGroup) {
|
||||||
when (permissionGroup) {
|
when (permissionGroup) {
|
||||||
@ -142,6 +146,7 @@ internal class PermissionsManagerImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionGroup.HiddenProfiles,
|
||||||
PermissionGroup.AppShortcuts -> {
|
PermissionGroup.AppShortcuts -> {
|
||||||
if (isAtLeastApiLevel(29)) {
|
if (isAtLeastApiLevel(29)) {
|
||||||
val roleManager = context.getSystemService<RoleManager>()
|
val roleManager = context.getSystemService<RoleManager>()
|
||||||
@ -196,6 +201,12 @@ internal class PermissionsManagerImpl(
|
|||||||
context.getSystemService<LauncherApps>()?.hasShortcutHostPermission() == true
|
context.getSystemService<LauncherApps>()?.hasShortcutHostPermission() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionGroup.HiddenProfiles -> {
|
||||||
|
if (isAtLeastApiLevel(29)) {
|
||||||
|
context.getSystemService<RoleManager>()?.isRoleHeld(RoleManager.ROLE_HOME) == true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
PermissionGroup.Accessibility -> {
|
PermissionGroup.Accessibility -> {
|
||||||
accessibilityPermissionState.value
|
accessibilityPermissionState.value
|
||||||
}
|
}
|
||||||
@ -211,6 +222,7 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.Notifications -> notificationsPermissionState
|
PermissionGroup.Notifications -> notificationsPermissionState
|
||||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
||||||
PermissionGroup.Accessibility -> accessibilityPermissionState
|
PermissionGroup.Accessibility -> accessibilityPermissionState
|
||||||
|
PermissionGroup.HiddenProfiles -> hiddenProfilesPermissionState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,12 +241,14 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.Notifications -> notificationsPermissionState.value = granted
|
PermissionGroup.Notifications -> notificationsPermissionState.value = granted
|
||||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
|
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
|
||||||
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
|
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
|
||||||
|
PermissionGroup.HiddenProfiles -> hiddenProfilesPermissionState.value = granted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
externalStoragePermissionState.value = checkPermissionOnce(PermissionGroup.ExternalStorage)
|
externalStoragePermissionState.value = checkPermissionOnce(PermissionGroup.ExternalStorage)
|
||||||
appShortcutsPermissionState.value = checkPermissionOnce(PermissionGroup.AppShortcuts)
|
appShortcutsPermissionState.value = checkPermissionOnce(PermissionGroup.AppShortcuts)
|
||||||
|
hiddenProfilesPermissionState.value = checkPermissionOnce(PermissionGroup.HiddenProfiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reportNotificationListenerState(running: Boolean) {
|
override fun reportNotificationListenerState(running: Boolean) {
|
||||||
|
|||||||
1
core/profiles/.gitignore
vendored
Normal file
1
core/profiles/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
50
core/profiles/build.gradle.kts
Normal file
50
core/profiles/build.gradle.kts
Normal 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"))
|
||||||
|
}
|
||||||
0
core/profiles/consumer-rules.pro
Normal file
0
core/profiles/consumer-rules.pro
Normal file
21
core/profiles/proguard-rules.pro
vendored
Normal file
21
core/profiles/proguard-rules.pro
vendored
Normal 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
|
||||||
4
core/profiles/src/main/AndroidManifest.xml
Normal file
4
core/profiles/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -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()) }
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,5 +47,6 @@ dependencies {
|
|||||||
implementation(project(":core:base"))
|
implementation(project(":core:base"))
|
||||||
implementation(project(":core:ktx"))
|
implementation(project(":core:ktx"))
|
||||||
implementation(project(":core:compat"))
|
implementation(project(":core:compat"))
|
||||||
|
implementation(project(":core:profiles"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5,7 +5,6 @@ import android.content.Context
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import de.mm20.launcher2.search.Application
|
import de.mm20.launcher2.search.Application
|
||||||
import de.mm20.launcher2.search.NullSerializer
|
import de.mm20.launcher2.search.NullSerializer
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
@ -15,7 +14,6 @@ class FakeApp: Application {
|
|||||||
override val componentName: ComponentName = ComponentName(randomString(), randomString())
|
override val componentName: ComponentName = ComponentName(randomString(), randomString())
|
||||||
override val isSystemApp: Boolean = false
|
override val isSystemApp: Boolean = false
|
||||||
override val isSuspended: Boolean = false
|
override val isSuspended: Boolean = false
|
||||||
override val profile: AppProfile = AppProfile.Personal
|
|
||||||
override val user: UserHandle = Process.myUserHandle()
|
override val user: UserHandle = Process.myUserHandle()
|
||||||
override val versionName: String = "1.0"
|
override val versionName: String = "1.0"
|
||||||
override val canUninstall: Boolean = false
|
override val canUninstall: Boolean = false
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import android.os.Looper
|
|||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import de.mm20.launcher2.ktx.normalize
|
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.Application
|
||||||
import de.mm20.launcher2.search.SearchableRepository
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@ -20,7 +22,9 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.runningFold
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@ -38,6 +42,7 @@ interface AppRepository : SearchableRepository<Application> {
|
|||||||
|
|
||||||
internal class AppRepositoryImpl(
|
internal class AppRepositoryImpl(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val profileManager: ProfileManager,
|
||||||
) : AppRepository {
|
) : AppRepository {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||||
|
|
||||||
@ -45,11 +50,8 @@ internal class AppRepositoryImpl(
|
|||||||
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
|
|
||||||
private val installedApps = MutableStateFlow<List<LauncherApp>>(emptyList())
|
private val installedApps = MutableStateFlow<List<LauncherApp>>(emptyList())
|
||||||
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
|
|
||||||
|
|
||||||
|
private val profiles = profileManager.activeProfiles
|
||||||
private val profiles: List<UserHandle>
|
|
||||||
get() = launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
@ -163,21 +165,42 @@ internal class AppRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}, Handler(Looper.getMainLooper()))
|
}, 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 {
|
scope.launch {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val apps = profiles.map { p ->
|
val apps = installedApps.value.toMutableList()
|
||||||
try {
|
apps.removeAll { it.user == profile.userHandle }
|
||||||
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it) }
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}.flatten()
|
|
||||||
installedApps.value = apps
|
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()
|
if (packageName == context.packageName) return emptyList()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import de.mm20.launcher2.icons.TintedIconLayer
|
|||||||
import de.mm20.launcher2.icons.TransparentLayer
|
import de.mm20.launcher2.icons.TransparentLayer
|
||||||
import de.mm20.launcher2.ktx.getSerialNumber
|
import de.mm20.launcher2.ktx.getSerialNumber
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import de.mm20.launcher2.search.Application
|
import de.mm20.launcher2.search.Application
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.StoreLink
|
import de.mm20.launcher2.search.StoreLink
|
||||||
@ -61,9 +60,6 @@ internal data class LauncherApp(
|
|||||||
|
|
||||||
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
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 isSystemApp: Boolean = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
|
||||||
|
|
||||||
override val canUninstall: Boolean
|
override val canUninstall: Boolean
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import org.koin.core.qualifier.named
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val applicationsModule = module {
|
val applicationsModule = module {
|
||||||
factory<SearchableRepository<Application>>(named<Application>()) { AppRepositoryImpl(androidContext()) }
|
factory<SearchableRepository<Application>>(named<Application>()) { get<AppRepository>() }
|
||||||
factory<AppRepository> { AppRepositoryImpl(androidContext()) }
|
single<AppRepository> { AppRepositoryImpl(androidContext(), get()) }
|
||||||
factory<SearchableDeserializer>(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) }
|
factory<SearchableDeserializer>(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) }
|
||||||
}
|
}
|
||||||
@ -5,10 +5,8 @@ import android.content.IntentSender
|
|||||||
import android.content.pm.LauncherActivityInfo
|
import android.content.pm.LauncherActivityInfo
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Process
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.ktx.romanize
|
import de.mm20.launcher2.ktx.romanize
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
@ -17,7 +15,6 @@ class AppShortcutConfigActivity(
|
|||||||
private val launcherActivityInfo: LauncherActivityInfo,
|
private val launcherActivityInfo: LauncherActivityInfo,
|
||||||
): Comparable<AppShortcutConfigActivity> {
|
): Comparable<AppShortcutConfigActivity> {
|
||||||
val label = launcherActivityInfo.label.toString()
|
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 {
|
fun getIcon(context: Context): Flow<Drawable?> = flow {
|
||||||
val icon = launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi)
|
val icon = launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi)
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import de.mm20.launcher2.crashreporter.CrashReporter
|
|||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.ktx.getSerialNumber
|
import de.mm20.launcher2.ktx.getSerialNumber
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import de.mm20.launcher2.search.AppShortcut
|
import de.mm20.launcher2.search.AppShortcut
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -69,9 +68,6 @@ internal data class LauncherShortcut(
|
|||||||
override val preferDetailsOverLaunch: Boolean = false
|
override val preferDetailsOverLaunch: Boolean = false
|
||||||
|
|
||||||
private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
|
private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
|
||||||
override val profile: AppProfile
|
|
||||||
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
|
|
||||||
|
|
||||||
|
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = if (isMainProfile) {
|
get() = if (isMainProfile) {
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import de.mm20.launcher2.icons.*
|
|||||||
import de.mm20.launcher2.ktx.getDrawableOrNull
|
import de.mm20.launcher2.ktx.getDrawableOrNull
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import de.mm20.launcher2.search.AppShortcut
|
import de.mm20.launcher2.search.AppShortcut
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
|
|
||||||
@ -26,9 +25,6 @@ internal data class LegacyShortcut(
|
|||||||
override val domain = Domain
|
override val domain = Domain
|
||||||
override val key: String = "$domain://${intent.toUri(0)}"
|
override val key: String = "$domain://${intent.toUri(0)}"
|
||||||
|
|
||||||
override val profile: AppProfile
|
|
||||||
get() = AppProfile.Personal
|
|
||||||
|
|
||||||
override fun overrideLabel(label: String): LegacyShortcut {
|
override fun overrideLabel(label: String): LegacyShortcut {
|
||||||
return this.copy(labelOverride = label)
|
return this.copy(labelOverride = label)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,9 @@ import android.content.pm.PackageManager
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.os.UserManager
|
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
import de.mm20.launcher2.icons.TintedIconLayer
|
||||||
import de.mm20.launcher2.search.AppProfile
|
|
||||||
import de.mm20.launcher2.search.AppShortcut
|
import de.mm20.launcher2.search.AppShortcut
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
@ -67,8 +65,6 @@ internal class UnavailableShortcut(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val isUnavailable: Boolean = true
|
override val isUnavailable: Boolean = true
|
||||||
override val profile: AppProfile
|
|
||||||
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal operator fun invoke(context: Context, id: String, packageName: String, user: UserHandle, userSerial: Long): UnavailableShortcut? {
|
internal operator fun invoke(context: Context, id: String, packageName: String, user: UserHandle, userSerial: Long): UnavailableShortcut? {
|
||||||
|
|||||||
@ -48,6 +48,7 @@ dependencies {
|
|||||||
implementation(project(":data:appshortcuts"))
|
implementation(project(":data:appshortcuts"))
|
||||||
implementation(project(":data:notifications"))
|
implementation(project(":data:notifications"))
|
||||||
implementation(project(":core:preferences"))
|
implementation(project(":core:preferences"))
|
||||||
|
implementation(project(":core:profiles"))
|
||||||
implementation(project(":core:base"))
|
implementation(project(":core:base"))
|
||||||
implementation(project(":data:files"))
|
implementation(project(":data:files"))
|
||||||
implementation(project(":data:searchable"))
|
implementation(project(":data:searchable"))
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import de.mm20.launcher2.badges.providers.HiddenItemBadgeProvider
|
|||||||
import de.mm20.launcher2.badges.providers.NotificationBadgeProvider
|
import de.mm20.launcher2.badges.providers.NotificationBadgeProvider
|
||||||
import de.mm20.launcher2.badges.providers.PluginBadgeProvider
|
import de.mm20.launcher2.badges.providers.PluginBadgeProvider
|
||||||
import de.mm20.launcher2.badges.providers.SuspendedAppsBadgeProvider
|
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.preferences.ui.BadgeSettings
|
||||||
import de.mm20.launcher2.search.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -42,7 +42,7 @@ internal class BadgeServiceImpl(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
settings.distinctUntilChanged().collectLatest {
|
settings.distinctUntilChanged().collectLatest {
|
||||||
val providers = mutableListOf<BadgeProvider>()
|
val providers = mutableListOf<BadgeProvider>()
|
||||||
providers += WorkProfileBadgeProvider()
|
providers += ProfileBadgeProvider()
|
||||||
providers += HiddenItemBadgeProvider()
|
providers += HiddenItemBadgeProvider()
|
||||||
if (it.notifications) {
|
if (it.notifications) {
|
||||||
providers += NotificationBadgeProvider()
|
providers += NotificationBadgeProvider()
|
||||||
|
|||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -68,3 +68,4 @@ include(":plugins:sdk")
|
|||||||
include(":data:locations")
|
include(":data:locations")
|
||||||
include(":services:plugins")
|
include(":services:plugins")
|
||||||
include(":core:devicepose")
|
include(":core:devicepose")
|
||||||
|
include(":core:profiles")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user