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(":libs:owncloud"))
|
||||
implementation(project(":core:permissions"))
|
||||
implementation(project(":core:profiles"))
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":services:search"))
|
||||
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.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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.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
|
||||
}
|
||||
@ -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?
|
||||
|
||||
|
||||
@ -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
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:ktx"))
|
||||
implementation(project(":core:compat"))
|
||||
implementation(project(":core:profiles"))
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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? {
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(":services:plugins")
|
||||
include(":core:devicepose")
|
||||
include(":core:profiles")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user