Filter hidden items in UI layer

This commit is contained in:
MM20 2022-06-12 13:28:02 +02:00
parent aa0cd9a062
commit 5ae0ec42b7
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
30 changed files with 96 additions and 245 deletions

View File

@ -120,7 +120,6 @@ dependencies {
implementation(project(":favorites"))
implementation(project(":files"))
implementation(project(":g-services"))
implementation(project(":hiddenitems"))
implementation(project(":i18n"))
implementation(project(":icons"))
implementation(project(":ktx"))

View File

@ -15,7 +15,6 @@ import de.mm20.launcher2.contacts.contactsModule
import de.mm20.launcher2.debug.Debug
import de.mm20.launcher2.favorites.favoritesModule
import de.mm20.launcher2.files.filesModule
import de.mm20.launcher2.hiddenitems.hiddenItemsModule
import de.mm20.launcher2.icons.iconsModule
import de.mm20.launcher2.music.musicModule
import de.mm20.launcher2.search.searchModule
@ -61,7 +60,6 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
databaseModule,
favoritesModule,
filesModule,
hiddenItemsModule,
iconsModule,
musicModule,
notificationsModule,

View File

@ -51,7 +51,6 @@ dependencies {
implementation(project(":base"))
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":hiddenitems"))
implementation(project(":compat"))
}

View File

@ -11,7 +11,6 @@ import android.os.Process
import android.os.UserHandle
import android.util.Log
import com.github.promeg.pinyinhelper.Pinyin
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.search.data.AppInstallation
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp
@ -23,13 +22,12 @@ import java.util.*
interface AppRepository {
fun search(query: String): Flow<List<Application>>
fun getAllInstalledApps(includeHidden: Boolean = false): Flow<List<Application>>
fun getAllInstalledApps(): Flow<List<Application>>
fun getSuspendedPackages(): Flow<List<String>>
}
internal class AppRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository,
) : AppRepository {
private val launcherApps =
@ -37,7 +35,6 @@ internal class AppRepositoryImpl(
private val installedApps = MutableStateFlow<List<LauncherApp>>(emptyList())
private val installations = MutableStateFlow<MutableList<AppInstallation>>(mutableListOf())
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
@ -191,7 +188,7 @@ internal class AppRepositoryImpl(
override fun search(query: String): Flow<List<Application>> = channelFlow {
merge(installedApps, hiddenItems, installations).collectLatest {
merge(installedApps, installations).collectLatest {
withContext(Dispatchers.Default) {
val appResults = mutableListOf<Application>()
if (query.isEmpty()) {
@ -204,13 +201,11 @@ internal class AppRepositoryImpl(
appResults.addAll(installations.value.filter {
matches(it.label, query)
})
val componentName = ComponentName.unflattenFromString(query)
getActivityByComponentName(componentName)?.let { appResults.add(it) }
}
val componentName = ComponentName.unflattenFromString(query)
getActivityByComponentName(componentName)?.let { appResults.add(it) }
appResults.removeAll { hiddenItems.value.contains(it.key) }
appResults.sort()
send(appResults)
@ -218,18 +213,8 @@ internal class AppRepositoryImpl(
}
}
override fun getAllInstalledApps(includeHidden: Boolean): Flow<List<Application>> {
return if (!includeHidden) {
channelFlow {
hiddenItems.collectLatest { hidden ->
installedApps.collectLatest { apps ->
send(apps.filter { !hidden.contains(it.key) })
}
}
}
} else {
installedApps
}
override fun getAllInstalledApps(): Flow<List<Application>> {
return installedApps
}
private fun matches(label: String, query: String): Boolean {

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val applicationsModule = module {
single<AppRepository> { AppRepositoryImpl(androidContext(), get()) }
single<AppRepository> { AppRepositoryImpl(androidContext()) }
}

View File

@ -46,7 +46,6 @@ dependencies {
implementation(project(":search"))
implementation(project(":permissions"))
implementation(project(":hiddenitems"))
implementation(project(":base"))
implementation(project(":preferences"))
implementation(project(":ktx"))

View File

@ -7,7 +7,6 @@ import android.content.pm.PackageManager
import android.os.Process
import androidx.core.content.getSystemService
import com.github.promeg.pinyinhelper.Pinyin
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
@ -33,7 +32,6 @@ interface AppShortcutRepository {
internal class AppShortcutRepositoryImpl(
private val context: Context,
private val permissionsManager: PermissionsManager,
private val hiddenItemsRepository: HiddenItemsRepository,
private val dataStore: LauncherDataStore,
) : AppShortcutRepository {
@ -110,22 +108,20 @@ internal class AppShortcutRepositoryImpl(
val pm = context.packageManager
hiddenItemsRepository.hiddenItemsKeys.collectLatest { hidden ->
send(
shortcuts.mapNotNull {
val label = try {
pm.getApplicationInfo(it.`package`, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
""
}
AppShortcut(
context,
it,
label
).takeIf { !hidden.contains(it.key) }
}.sorted()
)
}
send(
shortcuts.mapNotNull {
val label = try {
pm.getApplicationInfo(it.`package`, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
""
}
AppShortcut(
context,
it,
label
)
}.sorted()
)
}
}
}

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val appShortcutsModule = module {
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get(), get(), get()) }
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get(), get()) }
}

View File

@ -46,7 +46,6 @@ dependencies {
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":hiddenitems"))
implementation(project(":permissions"))
implementation(project(":material-color-utilities"))

View File

@ -1,12 +1,9 @@
package de.mm20.launcher2.calendar
import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.provider.CalendarContract
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
@ -29,23 +26,21 @@ interface CalendarRepository {
internal class CalendarRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : CalendarRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject()
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
override fun search(query: String): Flow<List<CalendarEvent>> = channelFlow {
override fun search(query: String): Flow<List<CalendarEvent>> {
if (query.isBlank() || query.length < 3) {
send(emptyList())
return@channelFlow
return flow {
emit(emptyList())
}
}
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
val searchCalendar = dataStore.data.map { it.calendarSearch.enabled }
combine(hasPermission, searchCalendar) { permission, search ->
return combine(hasPermission, searchCalendar) { permission, search ->
permission && search
}.map {
if (it) {
@ -58,12 +53,6 @@ internal class CalendarRepositoryImpl(
} else {
emptyList()
}
}.flatMapLatest { events ->
hiddenItems.map { hidden ->
events.filter { !hidden.contains(it.key) }
}
}.collectLatest {
send(it)
}
}
@ -160,23 +149,19 @@ internal class CalendarRepositoryImpl(
hasPermission.collectLatest {
if (it) {
dataStore.data.map { it.calendarWidget }.collectLatest { settings ->
hiddenItems.collectLatest { hidden ->
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
queryCalendarEvents(
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
excludeAllDayEvents = settings.hideAlldayEvents,
excludeCalendars = settings.excludeCalendarsList
).filter {
!hiddenItems.value.contains(it.key)
}
}
send(events)
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
queryCalendarEvents(
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
excludeAllDayEvents = settings.hideAlldayEvents,
excludeCalendars = settings.excludeCalendarsList
)
}
send(events)
}
} else {
send(emptyList())

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val calendarModule = module {
single<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
single<CalendarRepository> { CalendarRepositoryImpl(androidContext()) }
}

View File

@ -46,7 +46,6 @@ dependencies {
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":hiddenitems"))
implementation(project(":permissions"))
}

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.contacts
import android.content.Context
import android.provider.ContactsContract
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
@ -19,23 +18,22 @@ interface ContactRepository {
internal class ContactRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : ContactRepository, KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
override fun search(query: String): Flow<List<Contact>> = channelFlow {
override fun search(query: String): Flow<List<Contact>> {
val searchContacts = dataStore.data.map { it.contactsSearch.enabled }
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
if (query.length < 3) {
send(emptyList())
return@channelFlow
return flow {
emit(emptyList())
}
}
combine(searchContacts, hasPermission) { search, permission ->
return combine(searchContacts, hasPermission) { search, permission ->
search && permission
}.map {
if (it) {
@ -43,12 +41,6 @@ internal class ContactRepositoryImpl(
} else {
emptyList()
}
}.flatMapLatest { contacts ->
hiddenItems.map { hidden ->
contacts.filter { !hidden.contains(it.key) }
}
}.collectLatest {
send(it)
}
}

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val contactsModule = module {
single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) }
single<ContactRepository> { ContactRepositoryImpl(androidContext()) }
}

View File

@ -24,6 +24,9 @@ interface SearchDao {
@Query("SELECT * FROM Searchable WHERE pinned > 0 AND `key` LIKE 'calendar://%' ORDER BY pinned DESC, launchCount DESC")
fun getPinnedCalendarEvents(): Flow<List<FavoritesItemEntity>>
@Query("SELECT `key` FROM Searchable WHERE hidden = 1 AND `key` LIKE 'calendar://%'")
fun getHiddenCalendarEventKeys(): Flow<List<String>>
@Query("SELECT COUNT(key) as count FROM Searchable WHERE pinned = 1;")
fun getPinCount(): Int

View File

@ -27,6 +27,7 @@ interface FavoritesRepository {
): Flow<List<Searchable>>
fun getPinnedCalendarEvents(): Flow<List<Searchable>>
fun getHiddenCalendarEventKeys(): Flow<List<String>>
fun isPinned(searchable: Searchable): Flow<Boolean>
fun pinItem(searchable: Searchable)
fun unpinItem(searchable: Searchable)
@ -37,6 +38,7 @@ interface FavoritesRepository {
suspend fun getAllFavoriteItems(): List<FavoritesItem>
fun saveFavorites(favorites: List<FavoritesItem>)
fun getHiddenItems(): Flow<List<Searchable>>
fun getHiddenItemKeys(): Flow<List<String>>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
@ -91,6 +93,10 @@ internal class FavoritesRepositoryImpl(
}
}
override fun getHiddenCalendarEventKeys(): Flow<List<String>> {
return database.searchDao().getHiddenCalendarEventKeys()
}
override fun isPinned(searchable: Searchable): Flow<Boolean> {
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
}
@ -184,6 +190,10 @@ internal class FavoritesRepositoryImpl(
}
}
override fun getHiddenItemKeys(): Flow<List<String>> {
return database.searchDao().getHiddenItemKeys()
}
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
val deserializer: SearchableDeserializer =

View File

@ -45,7 +45,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":search"))
implementation(project(":hiddenitems"))
implementation(project(":preferences"))
implementation(project(":base"))
implementation(project(":ktx"))

View File

@ -1,9 +1,7 @@
package de.mm20.launcher2.files
import android.content.Context
import android.util.Log
import de.mm20.launcher2.files.providers.*
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.permissions.PermissionsManager
@ -22,14 +20,12 @@ interface FileRepository {
internal class FileRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository,
private val dataStore: LauncherDataStore,
private val permissionsManager: PermissionsManager,
) : FileRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
private val providers = MutableStateFlow<List<FileProvider>>(emptyList())
@ -75,12 +71,10 @@ internal class FileRepositoryImpl(
send(emptyList())
return@collectLatest
}
hiddenItems.collectLatest { hiddenItems ->
val results = mutableListOf<File>()
for (provider in providers) {
results.addAll(provider.search(query))
send(results.toList())
}
val results = mutableListOf<File>()
for (provider in providers) {
results.addAll(provider.search(query))
send(results.toList())
}
}
}

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val filesModule = module {
single<FileRepository> { FileRepositoryImpl(androidContext(), get(), get(), get()) }
single<FileRepository> { FileRepositoryImpl(androidContext(), get(), get()) }
}

View File

@ -1 +0,0 @@
/build

View File

@ -1,46 +0,0 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.hiddenitems"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.koin.android)
implementation(project(":database"))
implementation(project(":search"))
}

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.
#
# 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

View File

@ -1,4 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
/
</manifest>

View File

@ -1,38 +0,0 @@
package de.mm20.launcher2.hiddenitems
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.search.data.Searchable
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.collectLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* A low level repository for hidden items. This can only be used to retrieve keys and to check
* whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository.
*/
class HiddenItemsRepository(val context: Context, database: AppDatabase) {
val scope = CoroutineScope(Job() + Dispatchers.Default)
val hiddenItemsKeys = MutableStateFlow<List<String>>(emptyList())
init {
scope.launch {
AppDatabase.getInstance(context).searchDao().getHiddenItemKeys().collectLatest {
hiddenItemsKeys.value = it
}
}
}
fun isHidden(item: Searchable): Flow<Boolean> {
return AppDatabase.getInstance(context).searchDao().isHidden(item.key)
}
}

View File

@ -1,9 +0,0 @@
package de.mm20.launcher2.hiddenitems
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val hiddenItemsModule = module {
single { HiddenItemsRepository(androidContext(), get()) }
}

View File

@ -11,7 +11,6 @@ include(":contacts")
include(":g-services")
include(":files")
include(":calculator")
include(":hiddenitems")
include(":badges")
include(":applications")
include(":ms-services")

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.launcher.search
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -21,10 +22,7 @@ import de.mm20.launcher2.websites.WebsiteRepository
import de.mm20.launcher2.widgets.WidgetRepository
import de.mm20.launcher2.wikipedia.WikipediaRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -65,6 +63,10 @@ class SearchVM : ViewModel(), KoinComponent {
val hideFavorites = MutableLiveData(false)
private val hiddenItemKeys = favoritesRepository
.getHiddenItemKeys()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
init {
search("")
viewModelScope.launch {
@ -100,18 +102,24 @@ class SearchVM : ViewModel(), KoinComponent {
isSearching.postValue(true)
val jobs = mutableListOf<Deferred<Any>>()
jobs += async {
appRepository.search(query).collectLatest {
appResults.postValue(it)
appRepository.search(query).collectLatest { apps ->
hiddenItemKeys.collectLatest { hidden ->
appResults.postValue(apps.filter { !hidden.contains(it.key) })
}
}
}
jobs += async {
contactRepository.search(query).collectLatest {
contactResults.postValue(it)
contactRepository.search(query).collectLatest { contacts ->
hiddenItemKeys.collectLatest { hidden ->
contactResults.postValue(contacts.filter { !hidden.contains(it.key) })
}
}
}
jobs += async {
calendarRepository.search(query).collectLatest {
calendarResults.postValue(it)
calendarRepository.search(query).collectLatest { events ->
hiddenItemKeys.collectLatest { hidden ->
calendarResults.postValue(events.filter { !hidden.contains(it.key) })
}
}
}
jobs += async {
@ -135,8 +143,10 @@ class SearchVM : ViewModel(), KoinComponent {
}
}
jobs += async {
fileRepository.search(query).collectLatest {
fileResults.postValue(it)
fileRepository.search(query).collectLatest { files ->
hiddenItemKeys.collectLatest { hidden ->
fileResults.postValue(files.filter { !hidden.contains(it.key) })
}
}
}
jobs += async {
@ -145,8 +155,10 @@ class SearchVM : ViewModel(), KoinComponent {
}
}
jobs += async {
appShortcutRepository.search(query).collectLatest {
appShortcutResults.postValue(it)
appShortcutRepository.search(query).collectLatest { shortcuts ->
hiddenItemKeys.collectLatest { hidden ->
appShortcutResults.postValue(shortcuts.filter { !hidden.contains(it.key) })
}
}
}
jobs.map { it.await() }

View File

@ -143,8 +143,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
}
suspend fun onActive() {
calendarRepository.getUpcomingEvents().collectLatest {
upcomingEvents = it
calendarRepository.getUpcomingEvents().collectLatest { events ->
favoritesRepository.getHiddenCalendarEventKeys().collectLatest { hidden ->
upcomingEvents = events.filter { !hidden.contains(it.key) }
}
}
}

View File

@ -34,7 +34,7 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
private val favoritesRepository: FavoritesRepository by inject()
private val iconRepository: IconRepository by inject()
val allApps = appRepository.getAllInstalledApps(true).map {
val allApps = appRepository.getAllInstalledApps().map {
withContext(Dispatchers.Default) { it.sorted() }
}.asLiveData()
val hiddenItems: LiveData<List<Searchable>> = liveData {