Feature: Respect hidden private space option (#1923)
* Hide private profile space apps from search when hide private space option is enabled, and add a search action for locking and unlocking the private space. * Filter private space apps from favorites while hidden
This commit is contained in:
@@ -127,5 +127,6 @@ fun getSearchActionIconVector(icon: SearchActionIcon): Int {
|
||||
SearchActionIcon.Game -> R.drawable.sports_esports_24px
|
||||
SearchActionIcon.Note -> R.drawable.sticky_note_2_24px
|
||||
SearchActionIcon.Share -> R.drawable.share_24px
|
||||
SearchActionIcon.PrivateSpace -> R.drawable.encrypted_24px
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ fun SearchColumn(
|
||||
val apps = viewModel.appResults
|
||||
val workApps = viewModel.workAppResults
|
||||
val privateApps = viewModel.privateSpaceAppResults
|
||||
val profiles by viewModel.profiles.collectAsState(emptyList())
|
||||
val profiles by viewModel.visibleProfiles.collectAsState(emptyList())
|
||||
val profileStates by viewModel.profileStates.collectAsState(emptyList())
|
||||
|
||||
val appShortcuts = viewModel.appShortcutResults
|
||||
@@ -114,7 +114,7 @@ fun SearchColumn(
|
||||
|
||||
val expandedCategory: SearchCategory? by viewModel.expandedCategory
|
||||
|
||||
var selectedAppProfileIndex: Int by remember(isSearchEmpty) { mutableIntStateOf(0) }
|
||||
var selectedAppProfileIndex by viewModel.selectedAppProfileIndex
|
||||
var selectedAppIndex: Int by remember(query) { mutableIntStateOf(-1) }
|
||||
var selectedContactIndex: Int by remember(query) { mutableIntStateOf(-1) }
|
||||
var selectedFileIndex: Int by remember(query) { mutableIntStateOf(-1) }
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
@@ -91,7 +92,11 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
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)
|
||||
|
||||
@@ -143,8 +148,28 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
|
||||
val bestMatch = mutableStateOf<Searchable?>(null)
|
||||
|
||||
val selectedAppProfileIndex = mutableIntStateOf(0)
|
||||
|
||||
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) {
|
||||
|
||||
@@ -41,6 +41,7 @@ data class Profile(
|
||||
|
||||
data class State(
|
||||
val locked: Boolean = false,
|
||||
val hidden: Boolean = false,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -762,6 +762,9 @@
|
||||
<string name="search_action_contact">Add to contacts</string>
|
||||
<string name="search_action_open_url">View website</string>
|
||||
<string name="search_action_event">Schedule event</string>
|
||||
<string name="search_action_private_space">Private space</string>
|
||||
<string name="search_action_private_space_lock">Lock private space</string>
|
||||
<string name="search_action_private_space_unlock">Unlock private space</string>
|
||||
<string name="create_search_action_title">New quick action</string>
|
||||
<string name="edit_search_action_title">Edit quick action</string>
|
||||
<string name="create_search_action_type">What type of action do you want to create?</string>
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.LauncherUserInfo
|
||||
import android.os.Process
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
@@ -71,6 +72,11 @@ class ProfileManager(
|
||||
it.mapNotNull { it?.profile }
|
||||
}.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
|
||||
|
||||
val hiddenPrivateSpaceUser: Flow<UserHandle?> = profileStates.map { profiles ->
|
||||
val private = profiles[2]
|
||||
if (private?.state?.hidden == true) private.profile.userHandle else null
|
||||
}.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
|
||||
|
||||
init {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -164,9 +170,16 @@ class ProfileManager(
|
||||
}
|
||||
|
||||
private fun getProfileState(userHandle: UserHandle): Profile.State {
|
||||
return Profile.State(
|
||||
locked = !userManager.isUserUnlocked(userHandle),
|
||||
)
|
||||
val locked = !userManager.isUserUnlocked(userHandle)
|
||||
val hidden = if (isAtLeastApiLevel(36) && locked) {
|
||||
launcherApps.getLauncherUserInfo(userHandle)
|
||||
?.getUserConfig()
|
||||
?.getBoolean(LauncherUserInfo.PRIVATE_SPACE_ENTRYPOINT_HIDDEN, false)
|
||||
?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
return Profile.State(locked = locked, hidden = hidden)
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
|
||||
@@ -45,6 +45,7 @@ import de.mm20.launcher2.database.migrations.Migration_28_29
|
||||
import de.mm20.launcher2.database.migrations.Migration_29_30
|
||||
import de.mm20.launcher2.database.migrations.Migration_30_31
|
||||
import de.mm20.launcher2.database.migrations.Migration_31_32
|
||||
import de.mm20.launcher2.database.migrations.Migration_32_33
|
||||
import de.mm20.launcher2.database.migrations.Migration_6_7
|
||||
import de.mm20.launcher2.database.migrations.Migration_7_8
|
||||
import de.mm20.launcher2.database.migrations.Migration_8_9
|
||||
@@ -68,7 +69,7 @@ import java.util.UUID
|
||||
ShapesEntity::class,
|
||||
TransparenciesEntity::class,
|
||||
TypographyEntity::class,
|
||||
], version = 32, exportSchema = true
|
||||
], version = 33, exportSchema = true
|
||||
)
|
||||
@TypeConverters(ComponentNameConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -177,6 +178,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Migration_29_30(),
|
||||
Migration_30_31(),
|
||||
Migration_31_32(),
|
||||
Migration_32_33(),
|
||||
).build()
|
||||
if (_instance == null) _instance = instance
|
||||
return instance
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.mm20.launcher2.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration_32_33 : Migration(32, 33) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"INSERT INTO `SearchAction` (`position`, `type`) VALUES " +
|
||||
"((SELECT COALESCE(MIN(`position`) - 1, 0) FROM `SearchAction`), 'private_space')"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.PrivateSpaceLockActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
|
||||
@@ -60,6 +61,7 @@ internal class SearchActionRepositoryImpl(
|
||||
OpenUrlActionBuilder(context),
|
||||
WebsearchActionBuilder(context),
|
||||
ShareActionBuilder(context),
|
||||
PrivateSpaceLockActionBuilder(context),
|
||||
)
|
||||
|
||||
return allActions
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
|
||||
data class PrivateSpaceLockAction(
|
||||
override val label: String,
|
||||
val isLocked: Boolean,
|
||||
val userHandle: UserHandle,
|
||||
) : SearchAction {
|
||||
override val icon = SearchActionIcon.PrivateSpace
|
||||
override val iconColor = 0
|
||||
override val customIcon = null
|
||||
|
||||
override fun start(context: Context) {
|
||||
if (isAtLeastApiLevel(28)) {
|
||||
context.getSystemService(UserManager::class.java)
|
||||
?.requestQuietModeEnabled(!isLocked, userHandle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,8 @@ enum class SearchActionIcon(private val value: Int) {
|
||||
Music(19),
|
||||
Game(20),
|
||||
Note(21),
|
||||
Share(22);
|
||||
Share(22),
|
||||
PrivateSpace(23);
|
||||
fun toInt(): Int {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.os.UserManager
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.search.ResultScore
|
||||
import de.mm20.launcher2.searchactions.R
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.PrivateSpaceLockAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
class PrivateSpaceLockActionBuilder(
|
||||
override val label: String,
|
||||
) : SearchActionBuilder {
|
||||
|
||||
constructor(context: Context) : this(context.getString(R.string.search_action_private_space))
|
||||
|
||||
override val key = "private_space"
|
||||
override val icon = SearchActionIcon.PrivateSpace
|
||||
|
||||
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||
if (!isAtLeastApiLevel(35)) return null
|
||||
|
||||
val keyword = context.getString(R.string.search_action_private_space).lowercase()
|
||||
val score = ResultScore.from(
|
||||
query = classifiedQuery.text.lowercase(),
|
||||
primaryFields = listOf(keyword),
|
||||
)
|
||||
if (score.score < 0.8f) return null
|
||||
|
||||
val launcherApps = context.getSystemService(LauncherApps::class.java) ?: return null
|
||||
val userManager = context.getSystemService(UserManager::class.java) ?: return null
|
||||
|
||||
val privateHandle = launcherApps.profiles.firstOrNull {
|
||||
launcherApps.getLauncherUserInfo(it)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE
|
||||
} ?: return null
|
||||
|
||||
val isLocked = !userManager.isUserUnlocked(privateHandle)
|
||||
return PrivateSpaceLockAction(
|
||||
label = context.getString(
|
||||
if (isLocked) R.string.search_action_private_space_unlock
|
||||
else R.string.search_action_private_space_lock
|
||||
),
|
||||
isLocked = isLocked,
|
||||
userHandle = privateHandle,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ interface SearchActionBuilder {
|
||||
"website" -> return OpenUrlActionBuilder(context)
|
||||
"websearch" -> return WebsearchActionBuilder(context)
|
||||
"share" -> return ShareActionBuilder(context)
|
||||
"private_space" -> return PrivateSpaceLockActionBuilder(context)
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ dependencies {
|
||||
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":core:profiles"))
|
||||
implementation(project(":data:searchable"))
|
||||
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package de.mm20.launcher2.services.favorites
|
||||
|
||||
import de.mm20.launcher2.profiles.ProfileManager
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.searchable.PinnedLevel
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.searchable.VisibilityLevel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
|
||||
class FavoritesService(
|
||||
private val searchableRepository: SavableSearchableRepository,
|
||||
private val profileManager: ProfileManager,
|
||||
) {
|
||||
fun getFavorites(
|
||||
includeTypes: List<String>? = null,
|
||||
@@ -16,14 +21,23 @@ class FavoritesService(
|
||||
maxPinnedLevel: PinnedLevel = PinnedLevel.ManuallySorted,
|
||||
limit: Int = 100,
|
||||
): Flow<List<SavableSearchable>> {
|
||||
return searchableRepository.get(
|
||||
includeTypes = includeTypes,
|
||||
excludeTypes = excludeTypes,
|
||||
minPinnedLevel = minPinnedLevel,
|
||||
maxPinnedLevel = maxPinnedLevel,
|
||||
limit = limit,
|
||||
minVisibility = VisibilityLevel.SearchOnly,
|
||||
)
|
||||
return profileManager.activeProfiles.flatMapLatest {
|
||||
combine(
|
||||
searchableRepository.get(
|
||||
includeTypes = includeTypes,
|
||||
excludeTypes = excludeTypes,
|
||||
minPinnedLevel = minPinnedLevel,
|
||||
maxPinnedLevel = maxPinnedLevel,
|
||||
limit = Int.MAX_VALUE,
|
||||
minVisibility = VisibilityLevel.SearchOnly,
|
||||
),
|
||||
profileManager.hiddenPrivateSpaceUser,
|
||||
) { items, hiddenUser ->
|
||||
val filtered = if (hiddenUser == null) items
|
||||
else items.filter { it !is Application || it.user != hiddenUser }
|
||||
filtered.take(limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isPinned(searchable: SavableSearchable): Flow<Boolean> {
|
||||
|
||||
@@ -3,5 +3,5 @@ package de.mm20.launcher2.services.favorites
|
||||
import org.koin.dsl.module
|
||||
|
||||
val favoritesModule = module {
|
||||
factory { FavoritesService(get()) }
|
||||
factory { FavoritesService(get(), get()) }
|
||||
}
|
||||
Reference in New Issue
Block a user