diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index 587ec8c5c..129664a86 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -80,7 +81,7 @@ fun SearchColumn( val apps = viewModel.appResults val workApps = viewModel.workAppResults val privateApps = viewModel.privateSpaceAppResults - val profiles by viewModel.visibleProfiles.collectAsState(emptyList()) + val profiles by viewModel.profiles.collectAsState(emptyList()) val profileStates by viewModel.profileStates.collectAsState(emptyList()) val appShortcuts = viewModel.appShortcutResults @@ -191,7 +192,7 @@ fun SearchColumn( }, highlightedItem = bestMatch as? Application, profiles = profiles, - selectedProfileIndex = selectedAppProfileIndex, + selectedProfileIndex = selectedAppProfileIndex.takeIf { it in profiles.indices } ?: 0, onProfileSelected = { selectedAppProfileIndex = it onHideKeyboard() 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 111570930..d3fdd8aa9 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 @@ -83,20 +83,12 @@ class SearchVM : ViewModel(), KoinComponent { val expandedCategory = mutableStateOf(null) - val profiles = profileManager.profiles.shareIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - replay = 1 - ) + val profiles = profileManager.profiles val profileStates = profiles.flatMapLatest { combine(it.map { profileManager.getProfileState(it) }) { it.toList() } - }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1) - - val visibleProfiles = combine(profiles, profileStates) { profs, states -> - profs.filterIndexed { i, _ -> states.getOrNull(i)?.hidden != true } - }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1) + } val hasProfilesPermission = permissionsManager.hasPermission(PermissionGroup.ManageProfiles) @@ -152,24 +144,6 @@ class SearchVM : ViewModel(), KoinComponent { init { search("", forceRestart = true) - - /* - * Handle clearing the search query when the user changes the private space - * lock from the search action chip - */ - viewModelScope.launch { - var prevPrivateLocked: Boolean? = null - combine(profiles, profileStates) { profiles, states -> profiles to states } - .collect { (profiles, states) -> - val privateIdx = profiles.indexOfFirst { it.type == Profile.Type.Private } - val isLocked = states.getOrNull(privateIdx)?.locked - if (prevPrivateLocked != null && isLocked != null && prevPrivateLocked != isLocked && searchQuery.value.isNotEmpty()) { - search("") - if (!isLocked) selectedAppProfileIndex.intValue = privateIdx - } - prevPrivateLocked = isLocked - } - } } fun launchBestMatchOrAction(context: Context) { 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 index 4bf9c2a2e..0cb1e300c 100644 --- a/core/profiles/src/main/java/de/mm20/launcher2/profiles/ProfileManager.kt +++ b/core/profiles/src/main/java/de/mm20/launcher2/profiles/ProfileManager.kt @@ -20,10 +20,8 @@ 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 @@ -48,29 +46,29 @@ class ProfileManager( private val scope = CoroutineScope(Dispatchers.Default + Job()) - /** - * An array of exactly 3 profiles with their states. - * - Index 0: Personal profile - * - Index 1: Work profile - * - Index 2: Private profile - * - * Profiles that don't exist are null. - */ - private val profileStates: MutableStateFlow> = - MutableStateFlow(arrayOf(null, null, null)) + + private val profileMap = + MutableStateFlow(mapOf()) /** - * List of profiles that are active and unlocked. + * List of all profiles, sorted by type (Personal, Work, Private) */ - val activeProfiles: Flow> = profileStates.map { - it.mapNotNull { - if (it?.state?.locked != false) null else it.profile + val profiles: Flow> = profileMap.map { + it.values.map { it.profile }.sortedBy { it.type } + } + + val profileStates: Flow> = profileMap.map { + it.values.associate { it.profile to it.state } + } + + /** + * List of profiles that are currently unlocked + */ + val unlockedProfiles: Flow> = profileMap.map { + it.values.mapNotNull { + if (it.state.locked) null else it.profile } - }.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) - - val profiles: Flow> = profileStates.map { - it.mapNotNull { it?.profile } - }.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) + } init { val receiver = object : BroadcastReceiver() { @@ -111,37 +109,35 @@ class ProfileManager( private suspend fun refreshProfiles() { mutex.withLock { - val profiles = arrayOf(null, null, null) + val profiles = mutableMapOf() for (userHandle in launcherApps.profiles) { val serial = userManager.getSerialNumberForUser(userHandle) if (android.os.Build.MANUFACTURER == "samsung" && serial == 150L) continue // Hide Samsung Secure Folder val type = getProfileType(userHandle) - val index = when (type) { - Profile.Type.Personal -> 0 - Profile.Type.Work -> 1 - Profile.Type.Private -> 2 - } - if (profiles[index] == null) { - profiles[index] = ProfileWithState( + if (profiles[type] == null) { + profiles[type] = ProfileWithState( Profile( type = getProfileType(userHandle), userHandle = userHandle, serial = serial, ), - getProfileState(userHandle), + getProfileStateByUserHandle(userHandle), ) } } - profileStates.value = profiles + profileMap.value = profiles } } - fun getProfile(userHandle: UserHandle): Flow { - return profileStates.map { - it.find { it?.profile?.userHandle == userHandle }?.profile + /** + * Returns the profile for the given user handle, or null if it doesn't exist. + */ + fun getProfileByUserHandle(userHandle: UserHandle): Flow { + return profileMap.map { + it.values.find { it.profile.userHandle == userHandle }?.profile } } @@ -149,21 +145,48 @@ class ProfileManager( * Returns the profile of the given type, or null if it doesn't exist. */ fun getProfile(profileType: Profile.Type): Profile? { - return when (profileType) { - Profile.Type.Personal -> profileStates.value[0]?.profile - Profile.Type.Work -> profileStates.value[1]?.profile - Profile.Type.Private -> profileStates.value[2]?.profile + return profileMap.value[profileType]?.profile + } + + /** + * Returns the state of the given profile, or null if it doesn't exist. + */ + fun getProfileState(profile: Profile): Flow { + return profileMap.map { + it[profile.type]?.state } } - fun getProfileState(profile: Profile?): Flow { - return profileStates.map { profiles -> - profiles.find { it?.profile == profile }?.state + /** + * Returns the current state of the given profile, or null if it doesn't exist. + */ + fun getCurrentProfileState(profile: Profile): Profile.State? { + return profileMap.value[profile.type]?.state + } + + + /** + * Tries to unlock the given profile. Silently fails when there is an error. + */ + @RequiresApi(28) + fun unlockProfile(profile: Profile) { + try { + userManager.requestQuietModeEnabled(false, profile.userHandle) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Unable to unlock profile ${profile.serial}", e) } } - fun getProfileStateOnce(profile: Profile?): Profile.State? { - return profileStates.value.find { it?.profile == profile }?.state + /** + * Tries to lock the given profile. Silently fails when there is an error. + */ + @RequiresApi(28) + fun lockProfile(profile: Profile) { + try { + userManager.requestQuietModeEnabled(true, profile.userHandle) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Unable to lock profile ${profile.serial}", e) + } } private fun getProfileType(userHandle: UserHandle): Profile.Type { @@ -179,11 +202,11 @@ class ProfileManager( return if (userHandle == Process.myUserHandle()) Profile.Type.Personal else Profile.Type.Work } - private fun getProfileState(userHandle: UserHandle): Profile.State { + private fun getProfileStateByUserHandle(userHandle: UserHandle): Profile.State { val locked = !userManager.isUserUnlocked(userHandle) val hidden = if (isAtLeastApiLevel(36) && locked) { launcherApps.getLauncherUserInfo(userHandle) - ?.getUserConfig() + ?.userConfig ?.getBoolean(LauncherUserInfo.PRIVATE_SPACE_ENTRYPOINT_HIDDEN, false) ?: false } else { @@ -192,22 +215,4 @@ class ProfileManager( return Profile.State(locked = locked, hidden = hidden) } - @RequiresApi(28) - fun unlockProfile(profile: Profile) { - try { - userManager.requestQuietModeEnabled(false, profile.userHandle) - } catch (e: IllegalArgumentException) { - Log.w(TAG, "Unable to unlock profile ${profile.serial}", e) - } - } - - @RequiresApi(28) - fun lockProfile(profile: Profile) { - try { - userManager.requestQuietModeEnabled(true, profile.userHandle) - } catch (e: IllegalArgumentException) { - Log.w(TAG, "Unable to lock profile ${profile.serial}", e) - } - } - } 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 ca5a57e9b..1953f012c 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 @@ -52,7 +52,7 @@ internal class AppRepositoryImpl( private val installedApps = MutableStateFlow>(emptyList()) - private val profiles = profileManager.activeProfiles + private val profiles = profileManager.unlockedProfiles private val mutex = Mutex() diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index f48e884b9..db995b75a 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -223,7 +223,7 @@ internal class AppShortcutRepositoryImpl( val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps if (!launcherApps.hasShortcutHostPermission()) return emptyList() val results = mutableListOf() - val profiles = profileManager.activeProfiles.first() + val profiles = profileManager.unlockedProfiles.first() for (profile in profiles) { val activities = launcherApps.getShortcutConfigActivityList(null, profile.userHandle) results.addAll( diff --git a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/PrivateSpaceLockActionBuilder.kt b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/PrivateSpaceLockActionBuilder.kt index 2fb28e8f7..2fd5d9d89 100644 --- a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/PrivateSpaceLockActionBuilder.kt +++ b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/PrivateSpaceLockActionBuilder.kt @@ -28,7 +28,7 @@ class PrivateSpaceLockActionBuilder( if (!isAtLeastApiLevel(35)) return null val privateProfile = profileManager.getProfile(Profile.Type.Private) ?: return null - val profileState = profileManager.getProfileStateOnce(privateProfile) ?: return null + val profileState = profileManager.getCurrentProfileState(privateProfile) ?: return null val keyword = context.getString(R.string.search_query_private_space).lowercase() val score = ResultScore.from( 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 index af399a7e1..23b479b75 100644 --- 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 @@ -26,7 +26,7 @@ class ProfileBadgeProvider : BadgeProvider, KoinComponent { } if (userHandle != null) { emitAll( - profileManager.getProfile(userHandle).map { + profileManager.getProfileByUserHandle(userHandle).map { when (it?.type) { Profile.Type.Work -> WorkProfile Profile.Type.Private -> PrivateProfile