Migrate file search preferences
This commit is contained in:
parent
fe2da9a60e
commit
d031003845
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
@ -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? {
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user