From d031003845dfee3c3302c82f95ea763f8819e2d2 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 17 Jan 2022 00:08:03 +0100 Subject: [PATCH] Migrate file search preferences --- .../fragment/PreferencesSearchFragment.kt | 6 +- .../mm20/launcher2/files/FilesRepository.kt | 67 +++++- .../java/de/mm20/launcher2/files/Module.kt | 2 +- .../launcher2/files/providers/FileProvider.kt | 7 + .../files/providers/GDriveFileProvider.kt | 40 ++++ .../files/providers/LocalFileProvider.kt | 59 +++++ .../files/providers/NextcloudFileProvider.kt | 33 +++ .../files/providers/OneDriveFileProvider.kt | 48 ++++ .../files/providers/OwncloudFileProvider.kt | 29 +++ .../mm20/launcher2/search/data/GDriveFile.kt | 33 --- .../mm20/launcher2/search/data/LocalFile.kt | 53 ----- .../launcher2/search/data/NextcloudFile.kt | 22 -- .../launcher2/search/data/OneDriveFile.kt | 38 --- .../launcher2/search/data/OwncloudFile.kt | 22 -- i18n/src/main/res/values-de/strings.xml | 26 ++- i18n/src/main/res/values/strings.xml | 24 +- .../permissions/PermissionsManager.kt | 41 +++- .../preferences/LauncherPreferences.kt | 5 - .../de/mm20/launcher2/ui/base/BaseActivity.kt | 10 +- .../de/mm20/launcher2/ui/component/Banner.kt | 66 ++++++ .../ui/component/MissingPermissionBanner.kt | 59 ++--- .../launcher2/ui/settings/SettingsActivity.kt | 4 + .../filesearch/FileSearchSettingsScreen.kt | 216 ++++++++++++++++++ .../filesearch/FileSearchSettingsScreenVM.kt | 128 +++++++++++ .../settings/search/SearchSettingsScreen.kt | 5 +- 25 files changed, 785 insertions(+), 258 deletions(-) create mode 100644 files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt create mode 100644 files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt create mode 100644 files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt create mode 100644 files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt create mode 100644 files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt create mode 100644 files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/component/Banner.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesSearchFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesSearchFragment.kt index 4328088f..8756ffa8 100644 --- a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesSearchFragment.kt +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesSearchFragment.kt @@ -111,7 +111,6 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() { val user = client.getLoggedInUser() if (user == null) { nextcloudPref.setSummary(R.string.preference_summary_not_logged_in) - LauncherPreferences.instance.searchNextcloud = false nextcloudPref.setOnPreferenceChangeListener { _, value -> if (value as Boolean) { lifecycleScope.launch launch2@{ @@ -122,7 +121,7 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() { } } else { nextcloudPref.summary = context?.getString( - R.string.preference_search_nextcloud_summary, + R.string.preference_search_cloud_summary, user.displayName ) } @@ -136,7 +135,6 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() { val user = client.getLoggedInUser() if (user == null) { owncloudPref.setSummary(R.string.preference_summary_not_logged_in) - LauncherPreferences.instance.searchOwncloud = false owncloudPref.setOnPreferenceChangeListener { _, value -> if (value as Boolean) { lifecycleScope.launch launch2@{ @@ -148,7 +146,7 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() { } } else { owncloudPref.summary = context?.getString( - R.string.preference_search_nextcloud_summary, + R.string.preference_search_cloud_summary, user.displayName, ) } diff --git a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt index 722fc3e5..8576a552 100644 --- a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt +++ b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt @@ -1,14 +1,18 @@ package de.mm20.launcher2.files import android.content.Context +import de.mm20.launcher2.files.providers.* +import de.mm20.launcher2.files.providers.GDriveFileProvider +import de.mm20.launcher2.files.providers.LocalFileProvider +import de.mm20.launcher2.files.providers.NextcloudFileProvider +import de.mm20.launcher2.files.providers.OwncloudFileProvider import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.owncloud.OwncloudClient +import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.data.* import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.* interface FileRepository { fun search(query: String): Flow> @@ -17,11 +21,16 @@ interface FileRepository { class FileRepositoryImpl( private val context: Context, - hiddenItemsRepository: HiddenItemsRepository + hiddenItemsRepository: HiddenItemsRepository, + private val dataStore: LauncherDataStore ) : FileRepository { + private val scope = CoroutineScope(Job() + Dispatchers.Default) + private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys + private val providers = MutableStateFlow>(emptyList()) + private val nextcloudClient by lazy { NextcloudApiHelper(context) } @@ -29,13 +38,59 @@ class FileRepositoryImpl( OwncloudClient(context) } + init { + scope.launch { + dataStore.data.map { it.fileSearch }.distinctUntilChanged().collectLatest { + val provs = mutableListOf() + if (it.localFiles) { + provs += LocalFileProvider(context) + } + if (it.nextcloud) { + provs += NextcloudFileProvider(nextcloudClient) + } + if (it.owncloud) { + provs += OwncloudFileProvider(owncloudClient) + } + if (it.gdrive) { + provs += GDriveFileProvider(context) + } + if (it.onedrive) { + provs += OneDriveFileProvider(context) + } + providers.value = provs + } + } + } + override fun search(query: String): Flow> = channelFlow { if (query.isBlank()) { send(emptyList()) return@channelFlow } - hiddenItems.collectLatest { hiddenItems -> + //TODO SearchListView crashes if we send too many updates at once. Rewrite this code + // once SearchListView has been replaced with a Jetpack Compose version of itself + providers.collectLatest { providers -> + hiddenItems.collectLatest { hiddenItems -> + if (providers.first() is LocalFileProvider) { + val localFiles = providers.first().takeIf { it is LocalFileProvider }?.search(query) ?: emptyList() + delay(300) + if (providers.size > 1) { + val cloudFiles = providers.subList(1, providers.size).map { + async { it.search(query) } + }.awaitAll().flatten() + send(localFiles + cloudFiles) + } + } else { + val files = providers.map { + async { it.search(query) } + }.awaitAll().flatten() + send(files) + } + } + } + + /*hiddenItems.collectLatest { hiddenItems -> val files = mutableListOf() val localFiles = withContext(Dispatchers.IO) { @@ -56,7 +111,7 @@ class FileRepositoryImpl( yield() files.addAll(cloudFiles.filter { !hiddenItems.contains(it.key) }) send(files) - } + }*/ } override suspend fun deleteFile(file: File) { diff --git a/files/src/main/java/de/mm20/launcher2/files/Module.kt b/files/src/main/java/de/mm20/launcher2/files/Module.kt index 458f43a9..a5492618 100644 --- a/files/src/main/java/de/mm20/launcher2/files/Module.kt +++ b/files/src/main/java/de/mm20/launcher2/files/Module.kt @@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val filesModule = module { - single { FileRepositoryImpl(androidContext(), get()) } + single { FileRepositoryImpl(androidContext(), get(), get()) } viewModel { FilesViewModel(get()) } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt new file mode 100644 index 00000000..d30b9f8a --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.files.providers + +import de.mm20.launcher2.search.data.File + +interface FileProvider { + suspend fun search(query: String): List +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt new file mode 100644 index 00000000..0116f8f9 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt @@ -0,0 +1,40 @@ +package de.mm20.launcher2.files.providers + +import android.content.Context +import de.mm20.launcher2.files.R +import de.mm20.launcher2.gservices.DriveFileMeta +import de.mm20.launcher2.gservices.GoogleApiHelper +import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.data.GDriveFile + +internal class GDriveFileProvider( + private val context: Context +) : FileProvider { + override suspend fun search(query: String): List { + if (query.length < 4) return emptyList() + val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query) + return driveFiles.map { + GDriveFile( + fileId = it.fileId, + label = it.label, + size = it.size, + mimeType = it.mimeType, + isDirectory = it.isDirectory, + path = "", + directoryColor = it.directoryColor, + viewUri = it.viewUri, + metaData = getMetadata(it.metadata) + ) + }.sorted() + } + + private fun getMetadata(file: DriveFileMeta): List> { + val metaData = mutableListOf>() + val owners = file.owners + metaData.add(R.string.file_meta_owner to owners.joinToString(separator = ", ")) + val width = file.width ?: file.width + val height = file.height ?: file.height + if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height") + return metaData + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt new file mode 100644 index 00000000..3ded870d --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt @@ -0,0 +1,59 @@ +package de.mm20.launcher2.files.providers + +import android.content.Context +import android.provider.MediaStore +import androidx.core.database.getStringOrNull +import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.data.LocalFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal class LocalFileProvider( + private val context: Context +): FileProvider { + override suspend fun search(query: String): List = withContext(Dispatchers.IO) { + val results = mutableListOf() + val uri = MediaStore.Files.getContentUri("external").buildUpon() + .appendQueryParameter("limit", "10").build() + val projection = arrayOf( + MediaStore.Files.FileColumns.DISPLAY_NAME, + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.SIZE, + MediaStore.Files.FileColumns.DATA, + MediaStore.Files.FileColumns.MIME_TYPE + ) + val selection = + if (query.length > 3) "${MediaStore.Files.FileColumns.TITLE} LIKE ?" else "${MediaStore.Files.FileColumns.TITLE} = ?" + val selArgs = if (query.length > 3) arrayOf("%$query%") else arrayOf(query) + val sort = "${MediaStore.Files.FileColumns.DISPLAY_NAME} COLLATE NOCASE ASC" + + + val cursor = context.contentResolver.query(uri, projection, selection, selArgs, sort) + ?: return@withContext results + while (cursor.moveToNext()) { + if (results.size >= 10) { + break + } + val path = cursor.getString(3) + if (!java.io.File(path).exists()) continue + val directory = java.io.File(path).isDirectory + val mimeType = (cursor.getStringOrNull(4) + ?: if (directory) "inode/directory" else LocalFile.getMimetypeByFileExtension( + path.substringAfterLast( + '.' + ) + )) + val file = LocalFile( + path = path, + mimeType = mimeType, + size = cursor.getLong(2), + isDirectory = directory, + id = cursor.getLong(1), + metaData = LocalFile.getMetaData(context, mimeType, path) + ) + results.add(file) + } + cursor.close() + return@withContext results.sortedBy { it } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt new file mode 100644 index 00000000..e7f08399 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt @@ -0,0 +1,33 @@ +package de.mm20.launcher2.files.providers + +import de.mm20.launcher2.files.R +import de.mm20.launcher2.nextcloud.NextcloudApiHelper +import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.data.NextcloudFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.math.min + +internal class NextcloudFileProvider( + private val nextcloudClient: NextcloudApiHelper +) : FileProvider { + override suspend fun search(query: String): List { + if (query.length < 4) return emptyList() + val server = nextcloudClient.getServer() ?: return emptyList() + return withContext(Dispatchers.IO) { + nextcloudClient.files.search(query).let { it.subList(0, min(10, it.size)) }.map { + NextcloudFile( + fileId = it.id, + label = it.name, + path = server + it.url, + mimeType = it.mimeType, + size = it.size, + isDirectory = it.isDirectory, + server = server, + metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) } + ?: emptyList() + ) + } + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt new file mode 100644 index 00000000..a933b685 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt @@ -0,0 +1,48 @@ +package de.mm20.launcher2.files.providers + +import android.content.Context +import de.mm20.launcher2.files.R +import de.mm20.launcher2.msservices.DriveItem +import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.data.OneDriveFile + +internal class OneDriveFileProvider( + private val context: Context +): FileProvider { + override suspend fun search(query: String): List { + if (query.length < 4) return emptyList() + val driveItems = MicrosoftGraphApiHelper.getInstance(context).queryOneDriveFiles(query) ?: return emptyList() + val files = mutableListOf() + for (driveItem in driveItems) { + files += OneDriveFile( + fileId = driveItem.id, + label = driveItem.label, + path = "", + mimeType = driveItem.mimeType, + size = driveItem.size, + isDirectory = driveItem.isDirectory, + metaData = getMetaData(driveItem), + webUrl = driveItem.webUrl + ) + } + return files.sorted() + } + + private fun getMetaData(driveItem: DriveItem): List> { + val metaData = mutableListOf>() + driveItem.meta.owner?.let { + metaData.add(R.string.file_meta_owner to it) + } ?: driveItem.meta.createdBy?.let { + metaData.add(R.string.file_meta_owner to it) + } + val width = driveItem.meta.width + val height = driveItem.meta.height + + if (width != null && height != null) { + metaData.add(R.string.file_meta_dimensions to "${width}x${height}") + } + return metaData + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt new file mode 100644 index 00000000..de5bfae0 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt @@ -0,0 +1,29 @@ +package de.mm20.launcher2.files.providers + +import de.mm20.launcher2.files.R +import de.mm20.launcher2.helper.NetworkUtils +import de.mm20.launcher2.owncloud.OwncloudClient +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.data.OwncloudFile + +internal class OwncloudFileProvider( + private val owncloudClient: OwncloudClient +): FileProvider { + override suspend fun search(query: String): List { + if (query.length < 4) return emptyList() + val server = owncloudClient.getServer() ?: return emptyList() + return owncloudClient.files.query(query).map { + OwncloudFile( + fileId = it.id, + label = it.name, + path = server + it.url, + mimeType = it.mimeType, + size = it.size, + isDirectory = it.isDirectory, + server = server, + metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList() + ) + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt index d9033922..94fc0eb3 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt @@ -39,37 +39,4 @@ class GDriveFile( override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? { return null } - - companion object { - suspend fun search(context: Context, query: String): List { - if (query.length < 4) return emptyList() - val prefs = LauncherPreferences.instance - if (!prefs.searchGDrive) return emptyList() - if (NetworkUtils.isOffline(context, prefs.searchGDriveMobileData)) return emptyList() - val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query) - return driveFiles.map { - GDriveFile( - fileId = it.fileId, - label = it.label, - size = it.size, - mimeType = it.mimeType, - isDirectory = it.isDirectory, - path = "", - directoryColor = it.directoryColor, - viewUri = it.viewUri, - metaData = getMetadata(it.metadata) - ) - }.sorted() - } - - private fun getMetadata(file: DriveFileMeta): List> { - val metaData = mutableListOf>() - val owners = file.owners - metaData.add(R.string.file_meta_owner to owners.joinToString(separator = ", ")) - val width = file.width ?: file.width - val height = file.height ?: file.height - if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height") - return metaData - } - } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt index 4544fb60..c59c83d8 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt @@ -163,59 +163,6 @@ open class LocalFile( companion object: KoinComponent { - fun search(context: Context, query: String): List { - val results = mutableListOf() - if (!LauncherPreferences.instance.searchFiles) return results - if (query.isBlank()) return results - val permissionsManager: PermissionsManager = get() - if (!permissionsManager.checkPermissionOnce( - PermissionGroup.ExternalStorage - ) - ) return results - val uri = MediaStore.Files.getContentUri("external").buildUpon() - .appendQueryParameter("limit", "10").build() - val projection = arrayOf( - MediaStore.Files.FileColumns.DISPLAY_NAME, - MediaStore.Files.FileColumns._ID, - MediaStore.Files.FileColumns.SIZE, - MediaStore.Files.FileColumns.DATA, - MediaStore.Files.FileColumns.MIME_TYPE - ) - val selection = - if (query.length > 3) "${MediaStore.Files.FileColumns.TITLE} LIKE ?" else "${MediaStore.Files.FileColumns.TITLE} = ?" - val selArgs = if (query.length > 3) arrayOf("%$query%") else arrayOf(query) - val sort = "${MediaStore.Files.FileColumns.DISPLAY_NAME} COLLATE NOCASE ASC" - - - val cursor = context.contentResolver.query(uri, projection, selection, selArgs, sort) - ?: return results - while (cursor.moveToNext()) { - if (results.size >= 10) { - break - } - val path = cursor.getString(3) - if (!JavaIOFile(path).exists()) continue - val directory = JavaIOFile(path).isDirectory - val mimeType = (cursor.getStringOrNull(4) - ?: if (directory) "inode/directory" else getMimetypeByFileExtension( - path.substringAfterLast( - '.' - ) - )) - val file = LocalFile( - path = path, - mimeType = mimeType, - size = cursor.getLong(2), - isDirectory = directory, - id = cursor.getLong(1), - metaData = getMetaData(context, mimeType, path) - ) - results.add(file) - } - cursor.close() - return results.sortedBy { it } - } - internal fun getMimetypeByFileExtension(extension: String): String { return when (extension) { "apk" -> "application/vnd.android.package-archive" diff --git a/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt index cddeffbd..acd12ef1 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt @@ -31,26 +31,4 @@ class NextcloudFile( flags = Intent.FLAG_ACTIVITY_NEW_TASK } } - - companion object { - suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List { - if (!LauncherPreferences.instance.searchNextcloud) return emptyList() - if (query.length < 4) return emptyList() - val server = nextcloudClient.getServer() ?: return emptyList() - if (NetworkUtils.isOffline(context, LauncherPreferences.instance.searchGDriveMobileData)) return emptyList() - return nextcloudClient.files.search(query).map { - NextcloudFile( - fileId = it.id, - label = it.name, - path = server + it.url, - mimeType = it.mimeType, - size = it.size, - isDirectory = it.isDirectory, - server = server, - metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList() - ) - } - } - - } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt index b6a9272b..a6d7ac0b 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt @@ -36,42 +36,4 @@ class OneDriveFile( flags = Intent.FLAG_ACTIVITY_NEW_TASK } } - - companion object { - suspend fun search(context: Context, query: String): List { - if (query.length < 4) return emptyList() - if (!LauncherPreferences.instance.searchOneDrive) return emptyList() - val driveItems = MicrosoftGraphApiHelper.getInstance(context).queryOneDriveFiles(query) ?: return emptyList() - val files = mutableListOf() - for (driveItem in driveItems) { - files += OneDriveFile( - fileId = driveItem.id, - label = driveItem.label, - path = "", - mimeType = driveItem.mimeType, - size = driveItem.size, - isDirectory = driveItem.isDirectory, - metaData = getMetaData(driveItem), - webUrl = driveItem.webUrl - ) - } - return files.sorted() - } - - private fun getMetaData(driveItem: DriveItem): List> { - val metaData = mutableListOf>() - driveItem.meta.owner?.let { - metaData.add(R.string.file_meta_owner to it) - } ?: driveItem.meta.createdBy?.let { - metaData.add(R.string.file_meta_owner to it) - } - val width = driveItem.meta.width - val height = driveItem.meta.height - - if (width != null && height != null) { - metaData.add(R.string.file_meta_dimensions to "${width}x${height}") - } - return metaData - } - } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt index f0f5fcfc..a7e5a751 100644 --- a/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt +++ b/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt @@ -31,26 +31,4 @@ class OwncloudFile( flags = Intent.FLAG_ACTIVITY_NEW_TASK } } - - companion object { - suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List { - if (!LauncherPreferences.instance.searchOwncloud) return emptyList() - if (query.length < 4) return emptyList() - val server = owncloudClient.getServer() ?: return emptyList() - if (NetworkUtils.isOffline(context, LauncherPreferences.instance.searchGDriveMobileData)) return emptyList() - return owncloudClient.files.query(query).map { - OwncloudFile( - fileId = it.id, - label = it.name, - path = server + it.url, - mimeType = it.mimeType, - size = it.size, - isDirectory = it.isDirectory, - server = server, - metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList() - ) - } - } - - } } \ No newline at end of file diff --git a/i18n/src/main/res/values-de/strings.xml b/i18n/src/main/res/values-de/strings.xml index 0e42cc0f..cfcd587f 100644 --- a/i18n/src/main/res/values-de/strings.xml +++ b/i18n/src/main/res/values-de/strings.xml @@ -181,9 +181,6 @@ Angemeldet als %1$s Abmelden Eigentümer - Google Drive - Sie sind im Moment nicht angemeldet - %1$ss Dateien auf Google Drive durchsuchen Zeichnung E-Book Formular @@ -311,10 +308,6 @@ Bei Nextcloud anmelden Anmelden, um Ihren Nextcloud-Server durchsuchen zu können Status wird abgerufen… - OneDrive durchsuchen - %1$ss Dateien auf OneDrive durchsuchen - Nextcloud durchsuchen - %1$ss Dateien durchsuchen Google Drive OneDrive Telegram-Gruppe @@ -346,7 +339,6 @@ Anmelden, um Ihren Owncloud-Server durchsuchen zu können Nutzername darf nicht leer sein Passwort darf nicht leer sein - Owncloud durchsuchen Erscheinungsbild von Karten anpassen Karten Eckradius @@ -455,6 +447,16 @@ Shortcuts zu verschiedenen Websuch-Engines anzeigen Build-Information Weitere Informationen über diesen Build dieser App + Lokale Dateien + Dokumente, Fotos und andere Dateien auf diesem Gerät durchsuchen + Owncloud + OneDrive + %1$ss Dateien auf OneDrive durchsuchen + Nextcloud + %1$ss Dateien durchsuchen + Google Drive + Sie sind im Moment nicht angemeldet + %1$ss Dateien auf Google Drive durchsuchen Wikipedia-URL @@ -462,4 +464,12 @@ Bisher wurden keine Medien abgespielt Kalenderzugriff wird benötigt um Termine abzurufen + Speicher-Berechtigung wird benötigt um lokale Dateien zu durchsuchen + Alle Dateien verwalten-Berechtigung wird benötigt um lokale Dateien zu durchsuchen + + Sie haben noch kein Nextcloud-Konto verbunden + Sie haben noch kein Owncloud-Konto verbunden + Sie haben noch kein Microsoft-Konto verbunden + Sie haben noch kein Google-Konto verbunden + Konto verbinden \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index c7d0ff5d..fa5132b7 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -228,9 +228,7 @@ Signed in as %1$s Log out Owner - Google Drive You are currently not logged in - Search %1$s\'s files on Google Drive E-book Drawing Form @@ -310,10 +308,6 @@ Sign in to Nextcloud Sign in to search your Nextcloud server Checking status… - Search OneDrive - Search %1$s\'s files on OneDrive - Search Nextcloud - Search %1$s\'s files Google Drive OneDrive Telegram group @@ -346,7 +340,6 @@ Sign in to search your Owncloud server User name must not be empty Password must not be empty - Search Owncloud Customize card appearance Cards Corner radius @@ -439,6 +432,8 @@ Contact permission is required to search contacts Calendar permission is required to search calendar This widget requires calendar permission + Manage all files permission is required to search local files + External storage permission is required to search local files Set location Debug @@ -481,6 +476,15 @@ Show a preview of a website if the search query is a URL Web search Show shortcuts to different search engines + Local files + Search documents, photos and other files stored on this device + Google Drive + Search %1$s\'s files on Google Drive + OneDrive + Search %1$s\'s files on OneDrive + Nextcloud + Search %1$s\'s files + Owncloud Calendar Calendars @@ -501,4 +505,10 @@ %1$s is playing media No media has been played yet + + You haven\'t connected a Nextcloud account yet + You haven\'t connected an Owncloud account yet + You haven\'t connected a Microsoft account yet + You haven\'t connected a Google account yet + Connect account diff --git a/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt b/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt index 2f8958c0..e2fa2f51 100644 --- a/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt +++ b/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt @@ -9,14 +9,12 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings -import android.util.Log -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationManagerCompat import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.ktx.checkPermission import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.ktx.tryStartActivity import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +34,10 @@ interface PermissionsManager { grantResults: IntArray ) + fun onResume() { + + } + fun hasPermission(permissionGroup: PermissionGroup): Flow /** @@ -57,6 +59,8 @@ class PermissionsManagerImpl( private val context: Context ) : PermissionsManager { + private val pendingPermissionRequests = mutableSetOf() + private val calendarPermissionState = MutableStateFlow( checkPermissionOnce(PermissionGroup.Calendar) ) @@ -71,25 +75,25 @@ class PermissionsManagerImpl( ) private val notificationsPermissionState = MutableStateFlow(false) - override fun requestPermission(activity: AppCompatActivity, permissionGroup: PermissionGroup) { + override fun requestPermission(context: AppCompatActivity, permissionGroup: PermissionGroup) { when (permissionGroup) { PermissionGroup.Calendar -> { ActivityCompat.requestPermissions( - activity, + context, calendarPermissions, permissionGroup.ordinal ) } PermissionGroup.Location -> { ActivityCompat.requestPermissions( - activity, + context, locationPermissions, permissionGroup.ordinal ) } PermissionGroup.Contacts -> { ActivityCompat.requestPermissions( - activity, + context, contactPermissions, permissionGroup.ordinal ) @@ -98,12 +102,13 @@ class PermissionsManagerImpl( if (isAtLeastApiLevel(Build.VERSION_CODES.R)) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).also { - it.data = Uri.parse("package:${activity.packageName}") + it.data = Uri.parse("package:${context.packageName}") } - activity.startActivity(intent) + context.tryStartActivity(intent) + pendingPermissionRequests.add(PermissionGroup.ExternalStorage) } else { ActivityCompat.requestPermissions( - activity, + context, externalStoragePermissions, permissionGroup.ordinal ) @@ -111,7 +116,7 @@ class PermissionsManagerImpl( } PermissionGroup.Notifications -> { try { - activity.startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) + context.startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) } catch (e: ActivityNotFoundException) { CrashReporter.logException(e) } @@ -169,6 +174,20 @@ class PermissionsManagerImpl( } } + override fun onResume() { + val iterator = pendingPermissionRequests.iterator() + while (iterator.hasNext()) { + when (iterator.next()) { + PermissionGroup.ExternalStorage -> { + externalStoragePermissionState.value = + checkPermissionOnce(PermissionGroup.ExternalStorage) + } + else -> {} + } + iterator.remove() + } + } + override fun reportNotificationListenerState(running: Boolean) { notificationsPermissionState.value = running } diff --git a/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt b/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt index 78df9db2..b2bc54a6 100644 --- a/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt +++ b/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt @@ -58,11 +58,6 @@ class LauncherPreferences(val context: Application, version: Int = 3) { var searchActivities by BooleanPreference("search_activities", default = true) var searchCalendars by BooleanPreference("search_calendars", default = true) var searchContacts by BooleanPreference("search_contacts", default = true) - var searchOwncloud by BooleanPreference("search_owncloud", default = false) - var searchNextcloud by BooleanPreference("search_nextcloud", default = false) - var searchOneDrive by BooleanPreference("search_onedrive", default = false) - var searchGDrive by BooleanPreference("search_gdrive", default = false) - var searchGDriveMobileData by BooleanPreference("search_gdrive_mobile_data", default = false) var profileBadges by BooleanPreference("profile_badges", default = true) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/base/BaseActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/base/BaseActivity.kt index fc71dac9..aad55696 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/base/BaseActivity.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/base/BaseActivity.kt @@ -2,10 +2,9 @@ package de.mm20.launcher2.ui.base import androidx.appcompat.app.AppCompatActivity import de.mm20.launcher2.permissions.PermissionsManager -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import org.koin.android.ext.android.inject -abstract class BaseActivity : AppCompatActivity(), KoinComponent { +abstract class BaseActivity : AppCompatActivity() { private val permissionsManager: PermissionsManager by inject() override fun onRequestPermissionsResult( @@ -16,4 +15,9 @@ abstract class BaseActivity : AppCompatActivity(), KoinComponent { super.onRequestPermissionsResult(requestCode, permissions, grantResults) permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults) } + + override fun onResume() { + super.onResume() + permissionsManager.onResume() + } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/Banner.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/Banner.kt new file mode 100644 index 00000000..e4d9a7be --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/Banner.kt @@ -0,0 +1,66 @@ +package de.mm20.launcher2.ui.component + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun Banner( + modifier: Modifier = Modifier, + text: String, + icon: ImageVector, + primaryAction: @Composable () -> Unit, + secondaryAction: @Composable () -> Unit = {} +) { + Surface( + modifier = modifier, + color = MaterialTheme.colorScheme.secondaryContainer, + shape = RoundedCornerShape(8.dp), + shadowElevation = 2.dp, + tonalElevation = 2.dp + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.padding(16.dp), + imageVector = icon, + contentDescription = null + ) + Text( + text = text, + modifier = Modifier + .weight(1f) + .padding(vertical = 16.dp) + .padding(end = 16.dp), + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium) + ) + } + Row( + Modifier + .align(Alignment.End) + .padding(8.dp) + ) { + Box { + secondaryAction() + } + Box(modifier = Modifier.padding(start = 8.dp)) { + primaryAction() + } + + } + + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/MissingPermissionBanner.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/MissingPermissionBanner.kt index 42db7219..6a490b9f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/component/MissingPermissionBanner.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/MissingPermissionBanner.kt @@ -1,18 +1,14 @@ package de.mm20.launcher2.ui.component -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Lock -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import de.mm20.launcher2.ui.R @@ -23,46 +19,21 @@ fun MissingPermissionBanner( onClick: () -> Unit, secondaryAction: @Composable () -> Unit = {} ) { - Surface( + Banner( modifier = modifier, - color = MaterialTheme.colorScheme.secondaryContainer, - shape = RoundedCornerShape(8.dp), - shadowElevation = 2.dp, - tonalElevation = 2.dp - ) { - Column { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + text = text, + icon = Icons.Rounded.Lock, + primaryAction = { + TextButton( + modifier = Modifier.padding(start = 8.dp), + onClick = onClick ) { - Icon( - modifier = Modifier.padding(16.dp), - imageVector = Icons.Rounded.Lock, - contentDescription = null - ) Text( - text = text, - modifier = Modifier - .weight(1f) - .padding(vertical = 16.dp) - .padding(end = 16.dp), - style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium) + stringResource(R.string.grant_permission), + style = MaterialTheme.typography.labelLarge ) } - Row( - Modifier - .align(Alignment.End) - .padding(8.dp) - ) { - secondaryAction() - TextButton( - modifier = Modifier.padding(start = 8.dp), - onClick = onClick) { - Text(stringResource(R.string.grant_permission), style = MaterialTheme.typography.labelLarge) - } - - } - - } - } + }, + secondaryAction = secondaryAction + ) } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index ce0ddc0c..2640234f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -33,6 +33,7 @@ import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen import de.mm20.launcher2.ui.settings.accounts.AccountsSettingsScreen import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen +import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen @@ -97,6 +98,9 @@ class SettingsActivity : BaseActivity() { composable("settings/search/wikipedia") { WikipediaSettingsScreen() } + composable("settings/search/files") { + FileSearchSettingsScreen() + } composable("settings/widgets") { WidgetsSettingsScreen() } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt new file mode 100644 index 00000000..96badd5f --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt @@ -0,0 +1,216 @@ +package de.mm20.launcher2.ui.settings.filesearch + +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AccountBox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.accounts.AccountType +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.Banner +import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.PreferenceCategory +import de.mm20.launcher2.ui.component.preferences.PreferenceScreen +import de.mm20.launcher2.ui.component.preferences.SwitchPreference + +@Composable +fun FileSearchSettingsScreen() { + val viewModel: FileSearchSettingsScreenVM = viewModel() + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(null) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.onResume() + } + } + val loading by viewModel.loading.observeAsState() + PreferenceScreen(title = stringResource(R.string.preference_search_files)) { + if (loading == true) { + item { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth() + ) + } + return@PreferenceScreen + } + item { + PreferenceCategory { + val localFiles by viewModel.localFiles.observeAsState() + val hasFilePermission by viewModel.hasFilePermission.observeAsState() + AnimatedVisibility(hasFilePermission == false) { + MissingPermissionBanner( + text = stringResource( + if (isAtLeastApiLevel(29)) R.string.missing_permission_file_search_android10 else R.string.missing_permission_file_search + ), onClick = { + viewModel.requestFilePermission(context as AppCompatActivity) + }, + modifier = Modifier.padding(16.dp) + ) + } + SwitchPreference( + title = stringResource(R.string.preference_search_localfiles), + summary = stringResource(R.string.preference_search_localfiles_summary), + value = localFiles == true && hasFilePermission == true, + onValueChanged = { + viewModel.setLocalFiles(it) + }, + enabled = hasFilePermission == true + ) + + val nextcloud by viewModel.nextcloud.observeAsState() + val nextcloudAccount by viewModel.nextcloudAccount.observeAsState() + AnimatedVisibility(nextcloudAccount == null) { + Banner( + text = stringResource(R.string.no_account_nextcloud), + icon = Icons.Rounded.AccountBox, + primaryAction = { + TextButton(onClick = { + viewModel.login( + context as AppCompatActivity, + AccountType.Nextcloud + ) + }) { + Text( + stringResource(R.string.connect_account), + style = MaterialTheme.typography.labelLarge + ) + } + }, + modifier = Modifier.padding(16.dp) + ) + } + SwitchPreference( + title = stringResource(R.string.preference_search_nextcloud), + summary = nextcloudAccount?.let { + stringResource(R.string.preference_search_cloud_summary, it.userName) + } ?: stringResource(R.string.preference_summary_not_logged_in), + value = nextcloud == true && nextcloudAccount != null, + onValueChanged = { + viewModel.setNextcloud(it) + }, + enabled = nextcloudAccount != null + ) + + val owncloud by viewModel.owncloud.observeAsState() + val owncloudAccount by viewModel.owncloudAccount.observeAsState() + AnimatedVisibility(owncloudAccount == null) { + Banner( + text = stringResource(R.string.no_account_owncloud), + icon = Icons.Rounded.AccountBox, + primaryAction = { + TextButton(onClick = { + viewModel.login( + context as AppCompatActivity, + AccountType.Owncloud + ) + }) { + Text( + stringResource(R.string.connect_account), + style = MaterialTheme.typography.labelLarge + ) + } + }, + modifier = Modifier.padding(16.dp) + ) + } + SwitchPreference( + title = stringResource(R.string.preference_search_owncloud), + summary = owncloudAccount?.let { + stringResource(R.string.preference_search_cloud_summary, it.userName) + } ?: stringResource(R.string.preference_summary_not_logged_in), + value = owncloud == true && owncloudAccount != null, + onValueChanged = { + viewModel.setOwncloud(it) + }, + enabled = owncloudAccount != null + ) + + val onedrive by viewModel.onedrive.observeAsState() + val microsoftAccount by viewModel.microsoftAccount.observeAsState() + AnimatedVisibility(microsoftAccount == null) { + Banner( + text = stringResource(R.string.no_account_microsoft), + icon = Icons.Rounded.AccountBox, + primaryAction = { + TextButton(onClick = { + viewModel.login( + context as AppCompatActivity, + AccountType.Microsoft + ) + }) { + Text( + stringResource(R.string.connect_account), + style = MaterialTheme.typography.labelLarge + ) + } + }, + modifier = Modifier.padding(16.dp) + ) + } + SwitchPreference( + title = stringResource(R.string.preference_search_onedrive), + summary = microsoftAccount?.let { + stringResource(R.string.preference_search_onedrive_summary, it.userName) + } ?: stringResource(R.string.preference_summary_not_logged_in), + value = onedrive == true && microsoftAccount != null, + onValueChanged = { + viewModel.setOneDrive(it) + }, + enabled = microsoftAccount != null + ) + + val gdrive by viewModel.gdrive.observeAsState() + val googleAccount by viewModel.googleAccount.observeAsState() + AnimatedVisibility(googleAccount == null) { + Banner( + text = stringResource(R.string.no_account_google), + icon = Icons.Rounded.AccountBox, + primaryAction = { + TextButton(onClick = { + viewModel.login( + context as AppCompatActivity, + AccountType.Google + ) + }) { + Text( + stringResource(R.string.connect_account), + style = MaterialTheme.typography.labelLarge + ) + } + }, + modifier = Modifier.padding(16.dp) + ) + } + SwitchPreference( + title = stringResource(R.string.preference_search_gdrive), + summary = googleAccount?.let { + stringResource(R.string.preference_search_gdrive_summary, it.userName) + } ?: stringResource(R.string.preference_summary_not_logged_in), + value = gdrive == true && googleAccount != null, + onValueChanged = { + viewModel.setGdrive(it) + }, + enabled = googleAccount != null + ) + } + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt new file mode 100644 index 00000000..a05b2592 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt @@ -0,0 +1,128 @@ +package de.mm20.launcher2.ui.settings.filesearch + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.accounts.Account +import de.mm20.launcher2.accounts.AccountType +import de.mm20.launcher2.accounts.AccountsRepository +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.preferences.LauncherDataStore +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class FileSearchSettingsScreenVM : ViewModel(), KoinComponent { + private val dataStore: LauncherDataStore by inject() + private val accountsRepository: AccountsRepository by inject() + private val permissionsManager: PermissionsManager by inject() + + val hasFilePermission = + permissionsManager.hasPermission(PermissionGroup.ExternalStorage).asLiveData() + + val loading = MutableLiveData(true) + val nextcloudAccount = MutableLiveData(null) + val owncloudAccount = MutableLiveData(null) + val microsoftAccount = MutableLiveData(null) + val googleAccount = MutableLiveData(null) + + fun onResume() { + viewModelScope.launch { + nextcloudAccount.value = + accountsRepository.getCurrentlySignedInAccount(AccountType.Nextcloud) + owncloudAccount.value = + accountsRepository.getCurrentlySignedInAccount(AccountType.Owncloud) + microsoftAccount.value = + accountsRepository.getCurrentlySignedInAccount(AccountType.Microsoft) + googleAccount.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Google) + loading.value = false + } + } + + val localFiles = dataStore.data.map { it.fileSearch.localFiles }.asLiveData() + fun setLocalFiles(localFiles: Boolean) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setFileSearch( + it.fileSearch + .toBuilder() + .setLocalFiles(localFiles) + ) + .build() + } + } + } + + val nextcloud = dataStore.data.map { it.fileSearch.nextcloud }.asLiveData() + fun setNextcloud(nextcloud: Boolean) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setFileSearch( + it.fileSearch + .toBuilder() + .setNextcloud(nextcloud) + ) + .build() + } + } + } + + val gdrive = dataStore.data.map { it.fileSearch.gdrive }.asLiveData() + fun setGdrive(gdrive: Boolean) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setFileSearch( + it.fileSearch + .toBuilder() + .setGdrive(gdrive) + ) + .build() + } + } + } + + val onedrive = dataStore.data.map { it.fileSearch.onedrive }.asLiveData() + fun setOneDrive(onedrive: Boolean) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setFileSearch( + it.fileSearch + .toBuilder() + .setOnedrive(onedrive) + ) + .build() + } + } + } + + val owncloud = dataStore.data.map { it.fileSearch.owncloud }.asLiveData() + fun setOwncloud(owncloud: Boolean) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setFileSearch( + it.fileSearch + .toBuilder() + .setOwncloud(owncloud) + ) + .build() + } + } + } + + fun requestFilePermission(context: AppCompatActivity) { + permissionsManager.requestPermission(context, PermissionGroup.ExternalStorage) + } + + fun login(context: AppCompatActivity, accountType: AccountType) { + accountsRepository.signin(context, accountType) + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index b4dc0356..8328f7c8 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -44,7 +44,10 @@ fun SearchSettingsScreen() { Preference( title = stringResource(R.string.preference_search_files), summary = stringResource(R.string.preference_search_files_summary), - icon = Icons.Rounded.Description + icon = Icons.Rounded.Description, + onClick = { + navController?.navigate("settings/search/files") + } ) val hasContactsPermission by viewModel.hasContactsPermission.observeAsState()