Migrate file search preferences

This commit is contained in:
MM20
2022-01-17 00:08:03 +01:00
parent fe2da9a60e
commit d031003845
25 changed files with 785 additions and 258 deletions
@@ -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()
)
}
}
}
}