Migrate file search preferences
This commit is contained in:
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user