Migrate file search preferences

This commit is contained in:
MM20 2022-01-17 00:08:03 +01:00
parent fe2da9a60e
commit d031003845
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
25 changed files with 785 additions and 258 deletions

View File

@ -111,7 +111,6 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() {
val user = client.getLoggedInUser() val user = client.getLoggedInUser()
if (user == null) { if (user == null) {
nextcloudPref.setSummary(R.string.preference_summary_not_logged_in) nextcloudPref.setSummary(R.string.preference_summary_not_logged_in)
LauncherPreferences.instance.searchNextcloud = false
nextcloudPref.setOnPreferenceChangeListener { _, value -> nextcloudPref.setOnPreferenceChangeListener { _, value ->
if (value as Boolean) { if (value as Boolean) {
lifecycleScope.launch launch2@{ lifecycleScope.launch launch2@{
@ -122,7 +121,7 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() {
} }
} else { } else {
nextcloudPref.summary = context?.getString( nextcloudPref.summary = context?.getString(
R.string.preference_search_nextcloud_summary, R.string.preference_search_cloud_summary,
user.displayName user.displayName
) )
} }
@ -136,7 +135,6 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() {
val user = client.getLoggedInUser() val user = client.getLoggedInUser()
if (user == null) { if (user == null) {
owncloudPref.setSummary(R.string.preference_summary_not_logged_in) owncloudPref.setSummary(R.string.preference_summary_not_logged_in)
LauncherPreferences.instance.searchOwncloud = false
owncloudPref.setOnPreferenceChangeListener { _, value -> owncloudPref.setOnPreferenceChangeListener { _, value ->
if (value as Boolean) { if (value as Boolean) {
lifecycleScope.launch launch2@{ lifecycleScope.launch launch2@{
@ -148,7 +146,7 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() {
} }
} else { } else {
owncloudPref.summary = context?.getString( owncloudPref.summary = context?.getString(
R.string.preference_search_nextcloud_summary, R.string.preference_search_cloud_summary,
user.displayName, user.displayName,
) )
} }

View File

@ -1,14 +1,18 @@
package de.mm20.launcher2.files package de.mm20.launcher2.files
import android.content.Context 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.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.* import de.mm20.launcher2.search.data.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
interface FileRepository { interface FileRepository {
fun search(query: String): Flow<List<File>> fun search(query: String): Flow<List<File>>
@ -17,11 +21,16 @@ interface FileRepository {
class FileRepositoryImpl( class FileRepositoryImpl(
private val context: Context, private val context: Context,
hiddenItemsRepository: HiddenItemsRepository hiddenItemsRepository: HiddenItemsRepository,
private val dataStore: LauncherDataStore
) : FileRepository { ) : FileRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
private val providers = MutableStateFlow<List<FileProvider>>(emptyList())
private val nextcloudClient by lazy { private val nextcloudClient by lazy {
NextcloudApiHelper(context) NextcloudApiHelper(context)
} }
@ -29,13 +38,59 @@ class FileRepositoryImpl(
OwncloudClient(context) OwncloudClient(context)
} }
init {
scope.launch {
dataStore.data.map { it.fileSearch }.distinctUntilChanged().collectLatest {
val provs = mutableListOf<FileProvider>()
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<List<File>> = channelFlow { override fun search(query: String): Flow<List<File>> = channelFlow {
if (query.isBlank()) { if (query.isBlank()) {
send(emptyList()) send(emptyList())
return@channelFlow 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<File>() val files = mutableListOf<File>()
val localFiles = withContext(Dispatchers.IO) { val localFiles = withContext(Dispatchers.IO) {
@ -56,7 +111,7 @@ class FileRepositoryImpl(
yield() yield()
files.addAll(cloudFiles.filter { !hiddenItems.contains(it.key) }) files.addAll(cloudFiles.filter { !hiddenItems.contains(it.key) })
send(files) send(files)
} }*/
} }
override suspend fun deleteFile(file: File) { override suspend fun deleteFile(file: File) {

View File

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

View File

@ -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<File>
}

View File

@ -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<File> {
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<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>()
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
}
}

View File

@ -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<File> = withContext(Dispatchers.IO) {
val results = mutableListOf<LocalFile>()
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 }
}
}

View File

@ -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<File> {
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()
)
}
}
}
}

View File

@ -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<File> {
if (query.length < 4) return emptyList()
val driveItems = MicrosoftGraphApiHelper.getInstance(context).queryOneDriveFiles(query) ?: return emptyList()
val files = mutableListOf<OneDriveFile>()
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<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>()
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
}
}

View File

@ -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<File> {
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()
)
}
}
}

View File

@ -39,37 +39,4 @@ class GDriveFile(
override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? { override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? {
return null return null
} }
companion object {
suspend fun search(context: Context, query: String): List<File> {
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<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>()
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
}
}
} }

View File

@ -163,59 +163,6 @@ open class LocalFile(
companion object: KoinComponent { companion object: KoinComponent {
fun search(context: Context, query: String): List<LocalFile> {
val results = mutableListOf<LocalFile>()
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 { internal fun getMimetypeByFileExtension(extension: String): String {
return when (extension) { return when (extension) {
"apk" -> "application/vnd.android.package-archive" "apk" -> "application/vnd.android.package-archive"

View File

@ -31,26 +31,4 @@ class NextcloudFile(
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
} }
companion object {
suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List<NextcloudFile> {
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()
)
}
}
}
} }

View File

@ -36,42 +36,4 @@ class OneDriveFile(
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
} }
companion object {
suspend fun search(context: Context, query: String): List<File> {
if (query.length < 4) return emptyList()
if (!LauncherPreferences.instance.searchOneDrive) return emptyList()
val driveItems = MicrosoftGraphApiHelper.getInstance(context).queryOneDriveFiles(query) ?: return emptyList()
val files = mutableListOf<OneDriveFile>()
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<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>()
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
}
}
} }

View File

@ -31,26 +31,4 @@ class OwncloudFile(
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
} }
companion object {
suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List<OwncloudFile> {
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()
)
}
}
}
} }

View File

@ -181,9 +181,6 @@
<string name="preference_signin_user">Angemeldet als %1$s</string> <string name="preference_signin_user">Angemeldet als %1$s</string>
<string name="preference_signin_logout">Abmelden</string> <string name="preference_signin_logout">Abmelden</string>
<string name="file_meta_owner">Eigentümer</string> <string name="file_meta_owner">Eigentümer</string>
<string name="preference_search_gdrive">Google Drive</string>
<string name="preference_summary_not_logged_in">Sie sind im Moment nicht angemeldet</string>
<string name="preference_search_gdrive_summary">%1$ss Dateien auf Google Drive durchsuchen</string>
<string name="file_type_drawing">Zeichnung</string> <string name="file_type_drawing">Zeichnung</string>
<string name="file_type_ebook">E-Book</string> <string name="file_type_ebook">E-Book</string>
<string name="file_type_form">Formular</string> <string name="file_type_form">Formular</string>
@ -311,10 +308,6 @@
<string name="preference_nextcloud_signin">Bei Nextcloud anmelden</string> <string name="preference_nextcloud_signin">Bei Nextcloud anmelden</string>
<string name="preference_nextcloud_signin_summary">Anmelden, um Ihren Nextcloud-Server durchsuchen zu können</string> <string name="preference_nextcloud_signin_summary">Anmelden, um Ihren Nextcloud-Server durchsuchen zu können</string>
<string name="preference_account_checking_status">Status wird abgerufen…</string> <string name="preference_account_checking_status">Status wird abgerufen…</string>
<string name="preference_search_onedrive">OneDrive durchsuchen</string>
<string name="preference_search_onedrive_summary">%1$ss Dateien auf OneDrive durchsuchen</string>
<string name="preference_search_nextcloud">Nextcloud durchsuchen</string>
<string name="preference_search_nextcloud_summary">%1$ss Dateien durchsuchen</string>
<string name="storage_google_drive">Google Drive</string> <string name="storage_google_drive">Google Drive</string>
<string name="storage_onedrive">OneDrive</string> <string name="storage_onedrive">OneDrive</string>
<string name="preference_about_telegram">Telegram-Gruppe</string> <string name="preference_about_telegram">Telegram-Gruppe</string>
@ -346,7 +339,6 @@
<string name="preference_owncloud_signin_summary">Anmelden, um Ihren Owncloud-Server durchsuchen zu können</string> <string name="preference_owncloud_signin_summary">Anmelden, um Ihren Owncloud-Server durchsuchen zu können</string>
<string name="owncloud_username_empty">Nutzername darf nicht leer sein</string> <string name="owncloud_username_empty">Nutzername darf nicht leer sein</string>
<string name="owncloud_password_empty">Passwort darf nicht leer sein</string> <string name="owncloud_password_empty">Passwort darf nicht leer sein</string>
<string name="preference_search_owncloud">Owncloud durchsuchen</string>
<string name="preference_cards_summary">Erscheinungsbild von Karten anpassen</string> <string name="preference_cards_summary">Erscheinungsbild von Karten anpassen</string>
<string name="preference_cards">Karten</string> <string name="preference_cards">Karten</string>
<string name="preference_cards_corner_radius">Eckradius</string> <string name="preference_cards_corner_radius">Eckradius</string>
@ -455,6 +447,16 @@
<string name="preference_search_websearch_summary">Shortcuts zu verschiedenen Websuch-Engines anzeigen</string> <string name="preference_search_websearch_summary">Shortcuts zu verschiedenen Websuch-Engines anzeigen</string>
<string name="preference_screen_buildinfo">Build-Information</string> <string name="preference_screen_buildinfo">Build-Information</string>
<string name="preference_screen_buildinfo_summary">Weitere Informationen über diesen Build dieser App</string> <string name="preference_screen_buildinfo_summary">Weitere Informationen über diesen Build dieser App</string>
<string name="preference_search_localfiles">Lokale Dateien</string>
<string name="preference_search_localfiles_summary">Dokumente, Fotos und andere Dateien auf diesem Gerät durchsuchen</string>
<string name="preference_search_owncloud">Owncloud</string>
<string name="preference_search_onedrive">OneDrive</string>
<string name="preference_search_onedrive_summary">%1$ss Dateien auf OneDrive durchsuchen</string>
<string name="preference_search_nextcloud">Nextcloud</string>
<string name="preference_search_cloud_summary">%1$ss Dateien durchsuchen</string>
<string name="preference_search_gdrive">Google Drive</string>
<string name="preference_summary_not_logged_in">Sie sind im Moment nicht angemeldet</string>
<string name="preference_search_gdrive_summary">%1$ss Dateien auf Google Drive durchsuchen</string>
<string name="preference_wikipedia_customurl">Wikipedia-URL</string> <string name="preference_wikipedia_customurl">Wikipedia-URL</string>
@ -462,4 +464,12 @@
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string> <string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>
<string name="missing_permission_calendar_widget_settings">Kalenderzugriff wird benötigt um Termine abzurufen</string> <string name="missing_permission_calendar_widget_settings">Kalenderzugriff wird benötigt um Termine abzurufen</string>
<string name="missing_permission_file_search">Speicher-Berechtigung wird benötigt um lokale Dateien zu durchsuchen</string>
<string name="missing_permission_file_search_android10">Alle Dateien verwalten-Berechtigung wird benötigt um lokale Dateien zu durchsuchen</string>
<string name="no_account_nextcloud">Sie haben noch kein Nextcloud-Konto verbunden</string>
<string name="no_account_owncloud">Sie haben noch kein Owncloud-Konto verbunden</string>
<string name="no_account_microsoft">Sie haben noch kein Microsoft-Konto verbunden</string>
<string name="no_account_google">Sie haben noch kein Google-Konto verbunden</string>
<string name="connect_account">Konto verbinden</string>
</resources> </resources>

View File

@ -228,9 +228,7 @@
<string name="preference_signin_user">Signed in as %1$s</string> <string name="preference_signin_user">Signed in as %1$s</string>
<string name="preference_signin_logout">Log out</string> <string name="preference_signin_logout">Log out</string>
<string name="file_meta_owner">Owner</string> <string name="file_meta_owner">Owner</string>
<string name="preference_search_gdrive">Google Drive</string>
<string name="preference_summary_not_logged_in">You are currently not logged in</string> <string name="preference_summary_not_logged_in">You are currently not logged in</string>
<string name="preference_search_gdrive_summary">Search %1$s\'s files on Google Drive</string>
<string name="file_type_ebook">E-book</string> <string name="file_type_ebook">E-book</string>
<string name="file_type_drawing">Drawing</string> <string name="file_type_drawing">Drawing</string>
<string name="file_type_form">Form</string> <string name="file_type_form">Form</string>
@ -310,10 +308,6 @@
<string name="preference_nextcloud_signin">Sign in to Nextcloud</string> <string name="preference_nextcloud_signin">Sign in to Nextcloud</string>
<string name="preference_nextcloud_signin_summary">Sign in to search your Nextcloud server</string> <string name="preference_nextcloud_signin_summary">Sign in to search your Nextcloud server</string>
<string name="preference_account_checking_status">Checking status…</string> <string name="preference_account_checking_status">Checking status…</string>
<string name="preference_search_onedrive">Search OneDrive</string>
<string name="preference_search_onedrive_summary">Search %1$s\'s files on OneDrive</string>
<string name="preference_search_nextcloud">Search Nextcloud</string>
<string name="preference_search_nextcloud_summary">Search %1$s\'s files</string>
<string name="storage_google_drive">Google Drive</string> <string name="storage_google_drive">Google Drive</string>
<string name="storage_onedrive">OneDrive</string> <string name="storage_onedrive">OneDrive</string>
<string name="preference_about_telegram">Telegram group</string> <string name="preference_about_telegram">Telegram group</string>
@ -346,7 +340,6 @@
<string name="preference_owncloud_signin_summary">Sign in to search your Owncloud server</string> <string name="preference_owncloud_signin_summary">Sign in to search your Owncloud server</string>
<string name="owncloud_username_empty">User name must not be empty</string> <string name="owncloud_username_empty">User name must not be empty</string>
<string name="owncloud_password_empty">Password must not be empty</string> <string name="owncloud_password_empty">Password must not be empty</string>
<string name="preference_search_owncloud">Search Owncloud</string>
<string name="preference_cards_summary">Customize card appearance</string> <string name="preference_cards_summary">Customize card appearance</string>
<string name="preference_cards">Cards</string> <string name="preference_cards">Cards</string>
<string name="preference_cards_corner_radius">Corner radius</string> <string name="preference_cards_corner_radius">Corner radius</string>
@ -439,6 +432,8 @@
<string name="missing_permission_contact_search">Contact permission is required to search contacts</string> <string name="missing_permission_contact_search">Contact permission is required to search contacts</string>
<string name="missing_permission_calendar_search">Calendar permission is required to search calendar</string> <string name="missing_permission_calendar_search">Calendar permission is required to search calendar</string>
<string name="missing_permission_calendar_widget_settings">This widget requires calendar permission</string> <string name="missing_permission_calendar_widget_settings">This widget requires calendar permission</string>
<string name="missing_permission_file_search_android10">Manage all files permission is required to search local files</string>
<string name="missing_permission_file_search">External storage permission is required to search local files</string>
<string name="weather_widget_set_location">Set location</string> <string name="weather_widget_set_location">Set location</string>
<string name="preference_screen_debug">Debug</string> <string name="preference_screen_debug">Debug</string>
@ -481,6 +476,15 @@
<string name="preference_search_websites_summary">Show a preview of a website if the search query is a URL</string> <string name="preference_search_websites_summary">Show a preview of a website if the search query is a URL</string>
<string name="preference_search_websearch">Web search</string> <string name="preference_search_websearch">Web search</string>
<string name="preference_search_websearch_summary">Show shortcuts to different search engines</string> <string name="preference_search_websearch_summary">Show shortcuts to different search engines</string>
<string name="preference_search_localfiles">Local files</string>
<string name="preference_search_localfiles_summary">Search documents, photos and other files stored on this device</string>
<string name="preference_search_gdrive">Google Drive</string>
<string name="preference_search_gdrive_summary">Search %1$s\'s files on Google Drive</string>
<string name="preference_search_onedrive">OneDrive</string>
<string name="preference_search_onedrive_summary">Search %1$s\'s files on OneDrive</string>
<string name="preference_search_nextcloud">Nextcloud</string>
<string name="preference_search_cloud_summary">Search %1$s\'s files</string>
<string name="preference_search_owncloud">Owncloud</string>
<string name="preference_screen_calendarwidget">Calendar</string> <string name="preference_screen_calendarwidget">Calendar</string>
<string name="preference_calendar_calendars">Calendars</string> <string name="preference_calendar_calendars">Calendars</string>
@ -501,4 +505,10 @@
<string name="music_widget_default_title">%1$s is playing media</string> <string name="music_widget_default_title">%1$s is playing media</string>
<string name="music_widget_no_data">No media has been played yet</string> <string name="music_widget_no_data">No media has been played yet</string>
<string name="no_account_nextcloud">You haven\'t connected a Nextcloud account yet</string>
<string name="no_account_owncloud">You haven\'t connected an Owncloud account yet</string>
<string name="no_account_microsoft">You haven\'t connected a Microsoft account yet</string>
<string name="no_account_google">You haven\'t connected a Google account yet</string>
<string name="connect_account">Connect account</string>
</resources> </resources>

View File

@ -9,14 +9,12 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.Settings import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.checkPermission import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -36,6 +34,10 @@ interface PermissionsManager {
grantResults: IntArray grantResults: IntArray
) )
fun onResume() {
}
fun hasPermission(permissionGroup: PermissionGroup): Flow<Boolean> fun hasPermission(permissionGroup: PermissionGroup): Flow<Boolean>
/** /**
@ -57,6 +59,8 @@ class PermissionsManagerImpl(
private val context: Context private val context: Context
) : PermissionsManager { ) : PermissionsManager {
private val pendingPermissionRequests = mutableSetOf<PermissionGroup>()
private val calendarPermissionState = MutableStateFlow( private val calendarPermissionState = MutableStateFlow(
checkPermissionOnce(PermissionGroup.Calendar) checkPermissionOnce(PermissionGroup.Calendar)
) )
@ -71,25 +75,25 @@ class PermissionsManagerImpl(
) )
private val notificationsPermissionState = MutableStateFlow(false) private val notificationsPermissionState = MutableStateFlow(false)
override fun requestPermission(activity: AppCompatActivity, permissionGroup: PermissionGroup) { override fun requestPermission(context: AppCompatActivity, permissionGroup: PermissionGroup) {
when (permissionGroup) { when (permissionGroup) {
PermissionGroup.Calendar -> { PermissionGroup.Calendar -> {
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
activity, context,
calendarPermissions, calendarPermissions,
permissionGroup.ordinal permissionGroup.ordinal
) )
} }
PermissionGroup.Location -> { PermissionGroup.Location -> {
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
activity, context,
locationPermissions, locationPermissions,
permissionGroup.ordinal permissionGroup.ordinal
) )
} }
PermissionGroup.Contacts -> { PermissionGroup.Contacts -> {
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
activity, context,
contactPermissions, contactPermissions,
permissionGroup.ordinal permissionGroup.ordinal
) )
@ -98,12 +102,13 @@ class PermissionsManagerImpl(
if (isAtLeastApiLevel(Build.VERSION_CODES.R)) { if (isAtLeastApiLevel(Build.VERSION_CODES.R)) {
val intent = val intent =
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).also { 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 { } else {
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
activity, context,
externalStoragePermissions, externalStoragePermissions,
permissionGroup.ordinal permissionGroup.ordinal
) )
@ -111,7 +116,7 @@ class PermissionsManagerImpl(
} }
PermissionGroup.Notifications -> { PermissionGroup.Notifications -> {
try { try {
activity.startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) context.startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
CrashReporter.logException(e) 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) { override fun reportNotificationListenerState(running: Boolean) {
notificationsPermissionState.value = running notificationsPermissionState.value = running
} }

View File

@ -58,11 +58,6 @@ class LauncherPreferences(val context: Application, version: Int = 3) {
var searchActivities by BooleanPreference("search_activities", default = true) var searchActivities by BooleanPreference("search_activities", default = true)
var searchCalendars by BooleanPreference("search_calendars", default = true) var searchCalendars by BooleanPreference("search_calendars", default = true)
var searchContacts by BooleanPreference("search_contacts", 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) var profileBadges by BooleanPreference("profile_badges", default = true)

View File

@ -2,10 +2,9 @@ package de.mm20.launcher2.ui.base
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import org.koin.core.component.KoinComponent import org.koin.android.ext.android.inject
import org.koin.core.component.inject
abstract class BaseActivity : AppCompatActivity(), KoinComponent { abstract class BaseActivity : AppCompatActivity() {
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
@ -16,4 +15,9 @@ abstract class BaseActivity : AppCompatActivity(), KoinComponent {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults) permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }
override fun onResume() {
super.onResume()
permissionsManager.onResume()
}
} }

View File

@ -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()
}
}
}
}
}

View File

@ -1,18 +1,14 @@
package de.mm20.launcher2.ui.component 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.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Lock 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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
@ -23,46 +19,21 @@ fun MissingPermissionBanner(
onClick: () -> Unit, onClick: () -> Unit,
secondaryAction: @Composable () -> Unit = {} secondaryAction: @Composable () -> Unit = {}
) { ) {
Surface( Banner(
modifier = modifier, modifier = modifier,
color = MaterialTheme.colorScheme.secondaryContainer, text = text,
shape = RoundedCornerShape(8.dp), icon = Icons.Rounded.Lock,
shadowElevation = 2.dp, primaryAction = {
tonalElevation = 2.dp TextButton(
) { modifier = Modifier.padding(start = 8.dp),
Column { onClick = onClick
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(
modifier = Modifier.padding(16.dp),
imageVector = Icons.Rounded.Lock,
contentDescription = null
)
Text( Text(
text = text, stringResource(R.string.grant_permission),
modifier = Modifier style = MaterialTheme.typography.labelLarge
.weight(1f)
.padding(vertical = 16.dp)
.padding(end = 16.dp),
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium)
) )
} }
Row( },
Modifier secondaryAction = secondaryAction
.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)
}
}
}
}
} }

View File

@ -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.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.accounts.AccountsSettingsScreen import de.mm20.launcher2.ui.settings.accounts.AccountsSettingsScreen
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen 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.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
@ -97,6 +98,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/wikipedia") { composable("settings/search/wikipedia") {
WikipediaSettingsScreen() WikipediaSettingsScreen()
} }
composable("settings/search/files") {
FileSearchSettingsScreen()
}
composable("settings/widgets") { composable("settings/widgets") {
WidgetsSettingsScreen() WidgetsSettingsScreen()
} }

View File

@ -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
)
}
}
}
}

View File

@ -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<Account?>(null)
val owncloudAccount = MutableLiveData<Account?>(null)
val microsoftAccount = MutableLiveData<Account?>(null)
val googleAccount = MutableLiveData<Account?>(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)
}
}

View File

@ -44,7 +44,10 @@ fun SearchSettingsScreen() {
Preference( Preference(
title = stringResource(R.string.preference_search_files), title = stringResource(R.string.preference_search_files),
summary = stringResource(R.string.preference_search_files_summary), 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() val hasContactsPermission by viewModel.hasContactsPermission.observeAsState()