diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index a5a40951..88697dd1 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -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")) diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index 17362fce..4d5503bc 100644 --- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -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, ) ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index e918077f..3249aea9 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -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) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt index 28dfad66..2d618130 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt @@ -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 diff --git a/core/base/src/main/java/de/mm20/launcher2/profiles/Profile.kt b/core/base/src/main/java/de/mm20/launcher2/profiles/Profile.kt new file mode 100644 index 00000000..36404b78 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/profiles/Profile.kt @@ -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) + } + } +} diff --git a/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt b/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt index bc533a68..b61f3fb6 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt @@ -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 } \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Application.kt b/core/base/src/main/java/de/mm20/launcher2/search/Application.kt index 4ad14fa6..4a36d9e0 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/Application.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/Application.kt @@ -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? diff --git a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt index 975c006b..f4595e85 100644 --- a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt +++ b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt @@ -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() @@ -196,6 +201,12 @@ internal class PermissionsManagerImpl( context.getSystemService()?.hasShortcutHostPermission() == true } + PermissionGroup.HiddenProfiles -> { + if (isAtLeastApiLevel(29)) { + context.getSystemService()?.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) { diff --git a/core/profiles/.gitignore b/core/profiles/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/profiles/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/profiles/build.gradle.kts b/core/profiles/build.gradle.kts new file mode 100644 index 00000000..4c0f7329 --- /dev/null +++ b/core/profiles/build.gradle.kts @@ -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")) +} \ No newline at end of file diff --git a/core/profiles/consumer-rules.pro b/core/profiles/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/profiles/proguard-rules.pro b/core/profiles/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/profiles/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/core/profiles/src/main/AndroidManifest.xml b/core/profiles/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/core/profiles/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/profiles/src/main/java/de/mm20/launcher2/profiles/Module.kt b/core/profiles/src/main/java/de/mm20/launcher2/profiles/Module.kt new file mode 100644 index 00000000..8e7b31c8 --- /dev/null +++ b/core/profiles/src/main/java/de/mm20/launcher2/profiles/Module.kt @@ -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(androidContext(), get()) } +} \ No newline at end of file diff --git a/core/profiles/src/main/java/de/mm20/launcher2/profiles/ProfileManager.kt b/core/profiles/src/main/java/de/mm20/launcher2/profiles/ProfileManager.kt new file mode 100644 index 00000000..3bcd47d1 --- /dev/null +++ b/core/profiles/src/main/java/de/mm20/launcher2/profiles/ProfileManager.kt @@ -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()!! + private val launcherApps = context.getSystemService()!! + + private val scope = CoroutineScope(Dispatchers.Default + Job()) + + private val profileStates: MutableStateFlow> = + MutableStateFlow(emptyList()) + + /** + * List of profiles that are active and unlocked. + */ + val activeProfiles: Flow> = 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() + + 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 { + return profileStates.map { + it.find { it.profile.userHandle == userHandle }?.profile + } + } + + fun getProfileState(profile: Profile): Flow { + 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 + } +} \ No newline at end of file diff --git a/data/applications/build.gradle.kts b/data/applications/build.gradle.kts index b1b84587..ca47f64b 100644 --- a/data/applications/build.gradle.kts +++ b/data/applications/build.gradle.kts @@ -47,5 +47,6 @@ dependencies { implementation(project(":core:base")) implementation(project(":core:ktx")) implementation(project(":core:compat")) + implementation(project(":core:profiles")) } \ No newline at end of file diff --git a/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt index dd3afeef..d5392ef5 100644 --- a/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt +++ b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt @@ -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 diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt index 4759de89..211af10b 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -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 { 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>(emptyList()) - private val suspendedPackages = MutableStateFlow>(emptyList()) - - private val profiles: List - 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, Pair?, List?>>(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 { + private fun getApplications(packageName: String?, userHandle: UserHandle): List { if (packageName == context.packageName) return emptyList() return try { diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt index 57279cc5..70a7a525 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt @@ -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 diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt index 134ad889..440c6ec0 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt @@ -8,7 +8,7 @@ import org.koin.core.qualifier.named import org.koin.dsl.module val applicationsModule = module { - factory>(named()) { AppRepositoryImpl(androidContext()) } - factory { AppRepositoryImpl(androidContext()) } + factory>(named()) { get() } + single { AppRepositoryImpl(androidContext(), get()) } factory(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) } } \ No newline at end of file diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutConfigActivity.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutConfigActivity.kt index 08f378c7..9788e1dd 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutConfigActivity.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutConfigActivity.kt @@ -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 { val label = launcherActivityInfo.label.toString() - val profile: AppProfile = if (launcherActivityInfo.user == Process.myUserHandle()) AppProfile.Personal else AppProfile.Work fun getIcon(context: Context): Flow = flow { val icon = launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi) diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt index d72ec723..cf3c752b 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt @@ -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) { diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LegacyShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LegacyShortcut.kt index cc9e5094..f01c5489 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LegacyShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LegacyShortcut.kt @@ -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) } diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt index 6a47c23b..ed374942 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt @@ -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? { diff --git a/services/badges/build.gradle.kts b/services/badges/build.gradle.kts index 2793b180..2d7fc9a5 100644 --- a/services/badges/build.gradle.kts +++ b/services/badges/build.gradle.kts @@ -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")) diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt index b30a4327..4e64e8a0 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt @@ -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() - providers += WorkProfileBadgeProvider() + providers += ProfileBadgeProvider() providers += HiddenItemBadgeProvider() if (it.notifications) { providers += NotificationBadgeProvider() diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/ProfileBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/ProfileBadgeProvider.kt new file mode 100644 index 00000000..15e8d422 --- /dev/null +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/ProfileBadgeProvider.kt @@ -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 = 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) + ) + } +} \ No newline at end of file diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt deleted file mode 100644 index 85df6db5..00000000 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt +++ /dev/null @@ -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 = 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) - } - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b67ba76..cd3aa1eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -68,3 +68,4 @@ include(":plugins:sdk") include(":data:locations") include(":services:plugins") include(":core:devicepose") +include(":core:profiles")