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.Game -> R.drawable.sports_esports_24px
|
||||||
SearchActionIcon.Note -> R.drawable.sticky_note_2_24px
|
SearchActionIcon.Note -> R.drawable.sticky_note_2_24px
|
||||||
SearchActionIcon.Share -> R.drawable.share_24px
|
SearchActionIcon.Share -> R.drawable.share_24px
|
||||||
|
SearchActionIcon.PrivateSpace -> R.drawable.encrypted_24px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ fun SearchColumn(
|
|||||||
val apps = viewModel.appResults
|
val apps = viewModel.appResults
|
||||||
val workApps = viewModel.workAppResults
|
val workApps = viewModel.workAppResults
|
||||||
val privateApps = viewModel.privateSpaceAppResults
|
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 profileStates by viewModel.profileStates.collectAsState(emptyList())
|
||||||
|
|
||||||
val appShortcuts = viewModel.appShortcutResults
|
val appShortcuts = viewModel.appShortcutResults
|
||||||
@@ -114,7 +114,7 @@ fun SearchColumn(
|
|||||||
|
|
||||||
val expandedCategory: SearchCategory? by viewModel.expandedCategory
|
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 selectedAppIndex: Int by remember(query) { mutableIntStateOf(-1) }
|
||||||
var selectedContactIndex: Int by remember(query) { mutableIntStateOf(-1) }
|
var selectedContactIndex: Int by remember(query) { mutableIntStateOf(-1) }
|
||||||
var selectedFileIndex: 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 android.content.Context
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
@@ -91,7 +92,11 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
combine(it.map { profileManager.getProfileState(it) }) {
|
combine(it.map { profileManager.getProfileState(it) }) {
|
||||||
it.toList()
|
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)
|
val hasProfilesPermission = permissionsManager.hasPermission(PermissionGroup.ManageProfiles)
|
||||||
|
|
||||||
@@ -143,8 +148,28 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
val bestMatch = mutableStateOf<Searchable?>(null)
|
val bestMatch = mutableStateOf<Searchable?>(null)
|
||||||
|
|
||||||
|
val selectedAppProfileIndex = mutableIntStateOf(0)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
search("", forceRestart = true)
|
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) {
|
fun launchBestMatchOrAction(context: Context) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ data class Profile(
|
|||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
val locked: Boolean = false,
|
val locked: Boolean = false,
|
||||||
|
val hidden: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -762,6 +762,9 @@
|
|||||||
<string name="search_action_contact">Add to contacts</string>
|
<string name="search_action_contact">Add to contacts</string>
|
||||||
<string name="search_action_open_url">View website</string>
|
<string name="search_action_open_url">View website</string>
|
||||||
<string name="search_action_event">Schedule event</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="create_search_action_title">New quick action</string>
|
||||||
<string name="edit_search_action_title">Edit 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>
|
<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.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
|
import android.content.pm.LauncherUserInfo
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
@@ -71,6 +72,11 @@ class ProfileManager(
|
|||||||
it.mapNotNull { it?.profile }
|
it.mapNotNull { it?.profile }
|
||||||
}.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
|
}.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 {
|
init {
|
||||||
val receiver = object : BroadcastReceiver() {
|
val receiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
@@ -164,9 +170,16 @@ class ProfileManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getProfileState(userHandle: UserHandle): Profile.State {
|
private fun getProfileState(userHandle: UserHandle): Profile.State {
|
||||||
return Profile.State(
|
val locked = !userManager.isUserUnlocked(userHandle)
|
||||||
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)
|
@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_29_30
|
||||||
import de.mm20.launcher2.database.migrations.Migration_30_31
|
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_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_6_7
|
||||||
import de.mm20.launcher2.database.migrations.Migration_7_8
|
import de.mm20.launcher2.database.migrations.Migration_7_8
|
||||||
import de.mm20.launcher2.database.migrations.Migration_8_9
|
import de.mm20.launcher2.database.migrations.Migration_8_9
|
||||||
@@ -68,7 +69,7 @@ import java.util.UUID
|
|||||||
ShapesEntity::class,
|
ShapesEntity::class,
|
||||||
TransparenciesEntity::class,
|
TransparenciesEntity::class,
|
||||||
TypographyEntity::class,
|
TypographyEntity::class,
|
||||||
], version = 32, exportSchema = true
|
], version = 33, exportSchema = true
|
||||||
)
|
)
|
||||||
@TypeConverters(ComponentNameConverter::class)
|
@TypeConverters(ComponentNameConverter::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@@ -177,6 +178,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
Migration_29_30(),
|
Migration_29_30(),
|
||||||
Migration_30_31(),
|
Migration_30_31(),
|
||||||
Migration_31_32(),
|
Migration_31_32(),
|
||||||
|
Migration_32_33(),
|
||||||
).build()
|
).build()
|
||||||
if (_instance == null) _instance = instance
|
if (_instance == null) _instance = instance
|
||||||
return 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.ktx.jsonObjectOf
|
||||||
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
|
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
|
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.EmailActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
|
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
|
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
|
||||||
@@ -60,6 +61,7 @@ internal class SearchActionRepositoryImpl(
|
|||||||
OpenUrlActionBuilder(context),
|
OpenUrlActionBuilder(context),
|
||||||
WebsearchActionBuilder(context),
|
WebsearchActionBuilder(context),
|
||||||
ShareActionBuilder(context),
|
ShareActionBuilder(context),
|
||||||
|
PrivateSpaceLockActionBuilder(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
return allActions
|
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),
|
Music(19),
|
||||||
Game(20),
|
Game(20),
|
||||||
Note(21),
|
Note(21),
|
||||||
Share(22);
|
Share(22),
|
||||||
|
PrivateSpace(23);
|
||||||
fun toInt(): Int {
|
fun toInt(): Int {
|
||||||
return value
|
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)
|
"website" -> return OpenUrlActionBuilder(context)
|
||||||
"websearch" -> return WebsearchActionBuilder(context)
|
"websearch" -> return WebsearchActionBuilder(context)
|
||||||
"share" -> return ShareActionBuilder(context)
|
"share" -> return ShareActionBuilder(context)
|
||||||
|
"private_space" -> return PrivateSpaceLockActionBuilder(context)
|
||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(project(":core:base"))
|
implementation(project(":core:base"))
|
||||||
implementation(project(":core:i18n"))
|
implementation(project(":core:i18n"))
|
||||||
|
implementation(project(":core:profiles"))
|
||||||
implementation(project(":data:searchable"))
|
implementation(project(":data:searchable"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
package de.mm20.launcher2.services.favorites
|
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.search.SavableSearchable
|
||||||
import de.mm20.launcher2.searchable.PinnedLevel
|
import de.mm20.launcher2.searchable.PinnedLevel
|
||||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||||
import de.mm20.launcher2.searchable.VisibilityLevel
|
import de.mm20.launcher2.searchable.VisibilityLevel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
|
||||||
class FavoritesService(
|
class FavoritesService(
|
||||||
private val searchableRepository: SavableSearchableRepository,
|
private val searchableRepository: SavableSearchableRepository,
|
||||||
|
private val profileManager: ProfileManager,
|
||||||
) {
|
) {
|
||||||
fun getFavorites(
|
fun getFavorites(
|
||||||
includeTypes: List<String>? = null,
|
includeTypes: List<String>? = null,
|
||||||
@@ -16,14 +21,23 @@ class FavoritesService(
|
|||||||
maxPinnedLevel: PinnedLevel = PinnedLevel.ManuallySorted,
|
maxPinnedLevel: PinnedLevel = PinnedLevel.ManuallySorted,
|
||||||
limit: Int = 100,
|
limit: Int = 100,
|
||||||
): Flow<List<SavableSearchable>> {
|
): Flow<List<SavableSearchable>> {
|
||||||
return searchableRepository.get(
|
return profileManager.activeProfiles.flatMapLatest {
|
||||||
includeTypes = includeTypes,
|
combine(
|
||||||
excludeTypes = excludeTypes,
|
searchableRepository.get(
|
||||||
minPinnedLevel = minPinnedLevel,
|
includeTypes = includeTypes,
|
||||||
maxPinnedLevel = maxPinnedLevel,
|
excludeTypes = excludeTypes,
|
||||||
limit = limit,
|
minPinnedLevel = minPinnedLevel,
|
||||||
minVisibility = VisibilityLevel.SearchOnly,
|
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> {
|
fun isPinned(searchable: SavableSearchable): Flow<Boolean> {
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package de.mm20.launcher2.services.favorites
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val favoritesModule = module {
|
val favoritesModule = module {
|
||||||
factory { FavoritesService(get()) }
|
factory { FavoritesService(get(), get()) }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user