Migrate file search preferences
This commit is contained in:
parent
fe2da9a60e
commit
d031003845
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@ -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<List<File>>
|
||||
@ -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<List<FileProvider>>(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<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 {
|
||||
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<File>()
|
||||
|
||||
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) {
|
||||
|
||||
@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val filesModule = module {
|
||||
single<FileRepository> { FileRepositoryImpl(androidContext(), get()) }
|
||||
single<FileRepository> { FileRepositoryImpl(androidContext(), get(), get()) }
|
||||
viewModel { FilesViewModel(get()) }
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,59 +163,6 @@ open class LocalFile(
|
||||
|
||||
|
||||
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 {
|
||||
return when (extension) {
|
||||
"apk" -> "application/vnd.android.package-archive"
|
||||
|
||||
@ -31,26 +31,4 @@ class NextcloudFile(
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -36,42 +36,4 @@ class OneDriveFile(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,26 +31,4 @@ class OwncloudFile(
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -181,9 +181,6 @@
|
||||
<string name="preference_signin_user">Angemeldet als %1$s</string>
|
||||
<string name="preference_signin_logout">Abmelden</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_ebook">E-Book</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_summary">Anmelden, um Ihren Nextcloud-Server durchsuchen zu können</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_onedrive">OneDrive</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="owncloud_username_empty">Nutzername 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">Karten</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_screen_buildinfo">Build-Information</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>
|
||||
|
||||
@ -462,4 +464,12 @@
|
||||
<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_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>
|
||||
@ -228,9 +228,7 @@
|
||||
<string name="preference_signin_user">Signed in as %1$s</string>
|
||||
<string name="preference_signin_logout">Log out</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_search_gdrive_summary">Search %1$s\'s files on Google Drive</string>
|
||||
<string name="file_type_ebook">E-book</string>
|
||||
<string name="file_type_drawing">Drawing</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_summary">Sign in to search your Nextcloud server</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_onedrive">OneDrive</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="owncloud_username_empty">User name 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">Cards</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_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_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="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_websearch">Web search</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_calendar_calendars">Calendars</string>
|
||||
@ -501,4 +505,10 @@
|
||||
<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="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>
|
||||
|
||||
@ -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<Boolean>
|
||||
|
||||
/**
|
||||
@ -57,6 +59,8 @@ class PermissionsManagerImpl(
|
||||
private val context: Context
|
||||
) : PermissionsManager {
|
||||
|
||||
private val pendingPermissionRequests = mutableSetOf<PermissionGroup>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
66
ui/src/main/java/de/mm20/launcher2/ui/component/Banner.kt
Normal file
66
ui/src/main/java/de/mm20/launcher2/ui/component/Banner.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user