Reorganize and group modules
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
/
|
||||
</manifest>
|
||||
@@ -0,0 +1,295 @@
|
||||
package de.mm20.launcher2.files
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import org.json.JSONObject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class LocalFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as LocalFile
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.id
|
||||
).toString()
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = "file"
|
||||
}
|
||||
|
||||
class LocalFileDeserializer(
|
||||
val context: Context
|
||||
) : SearchableDeserializer, KoinComponent {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
val permissionsManager: PermissionsManager = get()
|
||||
if (!permissionsManager.checkPermissionOnce(
|
||||
PermissionGroup.ExternalStorage
|
||||
)
|
||||
) return null
|
||||
val json = JSONObject(serialized)
|
||||
val uri = MediaStore.Files.getContentUri("external")
|
||||
val proj = arrayOf(
|
||||
MediaStore.Files.FileColumns._ID,
|
||||
MediaStore.Files.FileColumns.SIZE,
|
||||
MediaStore.Files.FileColumns.DATA,
|
||||
MediaStore.Files.FileColumns.MIME_TYPE
|
||||
)
|
||||
val sel = "${MediaStore.Files.FileColumns._ID} = ?"
|
||||
val selArgs = arrayOf(json.getLong("id").toString())
|
||||
val cursor = context.contentResolver.query(uri, proj, sel, selArgs, null) ?: return null
|
||||
if (cursor.moveToNext()) {
|
||||
val path = cursor.getString(2)
|
||||
if (!java.io.File(path).exists()) return null
|
||||
val directory = java.io.File(path).isDirectory
|
||||
val id = cursor.getLong(0)
|
||||
val mimeType = cursor.getStringOrNull(3)
|
||||
?: if (directory) "resource/folder" else LocalFile.getMimetypeByFileExtension(
|
||||
path.substringAfterLast(
|
||||
'.'
|
||||
)
|
||||
)
|
||||
val size = cursor.getLong(1)
|
||||
cursor.close()
|
||||
return LocalFile(
|
||||
path = path,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
isDirectory = directory,
|
||||
id = id,
|
||||
metaData = LocalFile.getMetaData(context, mimeType, path)
|
||||
)
|
||||
}
|
||||
cursor.close()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class GDriveFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as GDriveFile
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.fileId,
|
||||
"label" to searchable.label,
|
||||
"path" to searchable.path,
|
||||
"mimeType" to searchable.mimeType,
|
||||
"size" to searchable.size,
|
||||
"directory" to searchable.isDirectory,
|
||||
"color" to searchable.directoryColor,
|
||||
"uri" to searchable.viewUri
|
||||
).apply {
|
||||
for ((k, v) in searchable.metaData) {
|
||||
put(
|
||||
when (k) {
|
||||
R.string.file_meta_owner -> "owner"
|
||||
R.string.file_meta_dimensions -> "dimensions"
|
||||
else -> "other"
|
||||
}, v
|
||||
)
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = "gdrive"
|
||||
}
|
||||
|
||||
class GDriveFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getString("id")
|
||||
val label = json.getString("label")
|
||||
val path = json.getString("path")
|
||||
val mimeType = json.getString("mimeType")
|
||||
val size = json.getLong("size")
|
||||
val directory = json.getBoolean("directory")
|
||||
val color = json.optString("color")
|
||||
val uri = json.getString("uri")
|
||||
val owner = json.optString("owner")
|
||||
val dimensions = json.optString("dimensions")
|
||||
val metaData = mutableListOf<Pair<Int, String>>()
|
||||
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
|
||||
dimensions.takeIf { it.isNotEmpty() }
|
||||
?.let { metaData.add(R.string.file_meta_dimensions to it) }
|
||||
return GDriveFile(
|
||||
fileId = id,
|
||||
label = label,
|
||||
path = path,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
directoryColor = color,
|
||||
isDirectory = directory,
|
||||
viewUri = uri,
|
||||
metaData = metaData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OneDriveFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as OneDriveFile
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.fileId,
|
||||
"label" to searchable.label,
|
||||
"mimeType" to searchable.mimeType,
|
||||
"size" to searchable.size,
|
||||
"directory" to searchable.isDirectory,
|
||||
"webUrl" to searchable.webUrl
|
||||
).apply {
|
||||
for ((k, v) in searchable.metaData) {
|
||||
put(
|
||||
when (k) {
|
||||
R.string.file_meta_owner -> "owner"
|
||||
R.string.file_meta_dimensions -> "dimensions"
|
||||
else -> "other"
|
||||
}, v
|
||||
)
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = "onedrive"
|
||||
}
|
||||
|
||||
class OneDriveFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val fileId = json.getString("id")
|
||||
val label = json.getString("label")
|
||||
val mimeType = json.getString("mimeType")
|
||||
val size = json.getLong("size")
|
||||
val isDirectory = json.getBoolean("directory")
|
||||
val webUrl = json.getString("webUrl")
|
||||
val owner = json.optString("owner")
|
||||
val dimensions = json.optString("dimensions")
|
||||
val metaData = mutableListOf<Pair<Int, String>>()
|
||||
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
|
||||
dimensions.takeIf { it.isNotEmpty() }
|
||||
?.let { metaData.add(R.string.file_meta_dimensions to it) }
|
||||
return OneDriveFile(
|
||||
fileId = fileId,
|
||||
label = label,
|
||||
path = "",
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
isDirectory = isDirectory,
|
||||
metaData = metaData,
|
||||
webUrl = webUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class NextcloudFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as NextcloudFile
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.fileId,
|
||||
"label" to searchable.label,
|
||||
"path" to searchable.path,
|
||||
"mimeType" to searchable.mimeType,
|
||||
"size" to searchable.size,
|
||||
"isDirectory" to searchable.isDirectory,
|
||||
"server" to searchable.server
|
||||
).apply {
|
||||
for ((k, v) in searchable.metaData) {
|
||||
put(
|
||||
when (k) {
|
||||
R.string.file_meta_owner -> "owner"
|
||||
else -> "other"
|
||||
}, v
|
||||
)
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = "nextcloud"
|
||||
}
|
||||
|
||||
class NextcloudFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
val label = json.getString("label")
|
||||
val path = json.getString("path")
|
||||
val mimeType = json.getString("mimeType")
|
||||
val size = json.getLong("size")
|
||||
val isDirectory = json.getBoolean("isDirectory")
|
||||
val server = json.getString("server")
|
||||
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
|
||||
|
||||
return NextcloudFile(
|
||||
fileId = id,
|
||||
label = label,
|
||||
path = path,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
isDirectory = isDirectory,
|
||||
server = server,
|
||||
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OwncloudFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as OwncloudFile
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.fileId,
|
||||
"label" to searchable.label,
|
||||
"path" to searchable.path,
|
||||
"mimeType" to searchable.mimeType,
|
||||
"size" to searchable.size,
|
||||
"isDirectory" to searchable.isDirectory,
|
||||
"server" to searchable.server
|
||||
).apply {
|
||||
for ((k, v) in searchable.metaData) {
|
||||
put(
|
||||
when (k) {
|
||||
R.string.file_meta_owner -> "owner"
|
||||
else -> "other"
|
||||
}, v
|
||||
)
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = "owncloud"
|
||||
}
|
||||
|
||||
class OwncloudFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
val label = json.getString("label")
|
||||
val path = json.getString("path")
|
||||
val mimeType = json.getString("mimeType")
|
||||
val size = json.getLong("size")
|
||||
val isDirectory = json.getBoolean("isDirectory")
|
||||
val server = json.getString("server")
|
||||
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
|
||||
|
||||
return OwncloudFile(
|
||||
fileId = id,
|
||||
label = label,
|
||||
path = path,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
isDirectory = isDirectory,
|
||||
server = server,
|
||||
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package de.mm20.launcher2.files
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.files.providers.FileProvider
|
||||
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.OneDriveFileProvider
|
||||
import de.mm20.launcher2.files.providers.OwncloudFileProvider
|
||||
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface FileRepository {
|
||||
fun search(
|
||||
query: String,
|
||||
local: Boolean = true,
|
||||
gdrive: Boolean = true,
|
||||
onedrive: Boolean = true,
|
||||
nextcloud: Boolean = true,
|
||||
owncloud: Boolean = true,
|
||||
): Flow<ImmutableList<File>>
|
||||
|
||||
fun deleteFile(file: File)
|
||||
}
|
||||
|
||||
internal class FileRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
) : FileRepository {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
private val nextcloudClient by lazy {
|
||||
NextcloudApiHelper(context)
|
||||
}
|
||||
private val owncloudClient by lazy {
|
||||
OwncloudClient(context)
|
||||
}
|
||||
|
||||
override fun search(
|
||||
query: String,
|
||||
local: Boolean,
|
||||
gdrive: Boolean,
|
||||
onedrive: Boolean,
|
||||
nextcloud: Boolean,
|
||||
owncloud: Boolean
|
||||
) = channelFlow {
|
||||
if (query.isBlank()) {
|
||||
send(persistentListOf())
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
val providers = mutableListOf<FileProvider>()
|
||||
|
||||
if (local) providers.add(LocalFileProvider(context, permissionsManager))
|
||||
if (gdrive) providers.add(GDriveFileProvider(context))
|
||||
if (onedrive) providers.add(OneDriveFileProvider(context))
|
||||
if (nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
|
||||
if (owncloud) providers.add(OwncloudFileProvider(owncloudClient))
|
||||
|
||||
if (providers.isEmpty()) {
|
||||
send(persistentListOf())
|
||||
return@channelFlow
|
||||
}
|
||||
val results = mutableListOf<File>()
|
||||
for (provider in providers) {
|
||||
results.addAll(provider.search(query))
|
||||
send(results.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteFile(file: File) {
|
||||
scope.launch {
|
||||
if (file.isDeletable) {
|
||||
file.delete(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.mm20.launcher2.files
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val filesModule = module {
|
||||
single<FileRepository> { FileRepositoryImpl(androidContext(), 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,66 @@
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
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,
|
||||
private val permissionsManager: PermissionsManager
|
||||
): FileProvider {
|
||||
override suspend fun search(query: String): List<File> = withContext(Dispatchers.IO) {
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.ExternalStorage)) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
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) "resource/folder" 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
|
||||
}
|
||||
}
|
||||
@@ -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.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
|
||||
}
|
||||
|
||||
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,27 @@
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.mm20.launcher2.media
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.media.ThumbnailUtils
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.provider.MediaStore
|
||||
import android.util.Size
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object ThumbnailUtilsCompat {
|
||||
fun createVideoThumbnail(file: File, size: Size, signal: CancellationSignal? = null): Bitmap? {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ThumbnailUtils.createVideoThumbnail(file, size, signal)
|
||||
} else {
|
||||
ThumbnailUtils.createVideoThumbnail(file.absolutePath,
|
||||
MediaStore.Video.Thumbnails.MICRO_KIND)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TintedIconLayer
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import java.util.*
|
||||
|
||||
interface File : SavableSearchable {
|
||||
val path: String
|
||||
val mimeType: String
|
||||
val size: Long
|
||||
val isDirectory: Boolean
|
||||
val metaData: List<Pair<Int, String>>
|
||||
|
||||
val isStoredInCloud: Boolean
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean
|
||||
get() = false
|
||||
|
||||
open val providerIconRes: Int?
|
||||
get() = null
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||
val (resId, bgColor) = when {
|
||||
isDirectory -> R.drawable.ic_file_folder to R.color.lightblue
|
||||
mimeType.startsWith("image/") -> R.drawable.ic_file_picture to R.color.teal
|
||||
mimeType.startsWith("audio/") -> R.drawable.ic_file_music to R.color.orange
|
||||
mimeType.startsWith("video/") -> R.drawable.ic_file_video to R.color.purple
|
||||
else -> when (mimeType) {
|
||||
"application/zip", "application/x-gtar", "application/x-tar",
|
||||
"application/java-archive", "application/x-7z-compressed",
|
||||
"application/x-compressed-tar", "application/x-gzip", "application/x-bzip2" -> R.drawable.ic_file_archive to R.color.brown
|
||||
"application/pdf" -> R.drawable.ic_file_pdf to R.color.red
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword", "text/plain", "application/vnd.google-apps.document" -> R.drawable.ic_file_document to R.color.blue
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel", "application/vnd.google-apps.spreadsheet" -> R.drawable.ic_file_spreadsheet to R.color.green
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.ms-powerpoint", "application/vnd.google-apps.presentation" -> R.drawable.ic_file_presentation to R.color.amber
|
||||
"text/x-asm", "text/x-c", "text/x-java-source", "text/x-script.phyton", "text/x-pascal",
|
||||
"text/x-script.perl", "text/javascript", "application/json" -> R.drawable.ic_file_code to R.color.pink
|
||||
"text/xml", "text/html" -> R.drawable.ic_file_markup to R.color.deeporange
|
||||
"application/vnd.android.package-archive" -> R.drawable.ic_file_android to R.color.lightgreen
|
||||
"application/vnd.google-apps.form" -> R.drawable.ic_file_form to R.color.deeppurple
|
||||
"application/vnd.google-apps.drawing" -> R.drawable.ic_file_picture to R.color.teal
|
||||
else -> R.drawable.ic_file_generic to R.color.bluegrey
|
||||
}
|
||||
}
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedIconLayer(
|
||||
icon = ContextCompat.getDrawable(context, resId)!!,
|
||||
scale = 0.5f,
|
||||
color = ContextCompat.getColor(context, bgColor)
|
||||
),
|
||||
backgroundLayer = ColorLayer(ContextCompat.getColor(context, bgColor))
|
||||
)
|
||||
}
|
||||
|
||||
fun getFileType(context: Context): String {
|
||||
if (isDirectory) return context.getString(R.string.file_type_directory)
|
||||
if (mimeType == "application/vendor.de.mm20.launcher2.backup") {
|
||||
return context.getString(
|
||||
R.string.file_type_launcherbackup,
|
||||
context.getString(R.string.app_name)
|
||||
)
|
||||
}
|
||||
val resource = when (mimeType) {
|
||||
"application/zip",
|
||||
"application/x-zip-compressed",
|
||||
"application/x-gtar",
|
||||
"application/x-tar",
|
||||
"application/java-archive",
|
||||
"application/x-7z-compressed" -> R.string.file_type_archive
|
||||
"application/x-gzip",
|
||||
"application/x-bzip2" -> R.string.file_type_compressed
|
||||
"application/vnd.android.package-archive" -> R.string.file_type_android
|
||||
"text/x-asm",
|
||||
"text/x-c",
|
||||
"text/x-java-source",
|
||||
"text/x-script.phyton",
|
||||
"text/x-pascal",
|
||||
"text/x-script.perl",
|
||||
"text/javascript",
|
||||
"application/json" -> R.string.file_type_source_code
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword",
|
||||
"application/x-iwork-pages-sffpages",
|
||||
"application/vnd.apple.pages",
|
||||
"application/vnd.google-apps.document" -> R.string.file_type_document
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel",
|
||||
"application/x-iwork-numbers-sffnumbers",
|
||||
"application/vnd.apple.numbers",
|
||||
"application/vnd.google-apps.spreadsheet" -> R.string.file_type_spreadsheet
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/x-iwork-keynote-sffkey",
|
||||
"application/vnd.apple.keynote",
|
||||
"application/vnd.google-apps.presentation" -> R.string.file_type_presentation
|
||||
"text/plain" -> R.string.file_type_text
|
||||
"application/vnd.google-apps.drawing" -> R.string.file_type_drawing
|
||||
"application/vnd.google-apps.form" -> R.string.file_type_form
|
||||
"application/epub+zip" -> R.string.file_type_ebook
|
||||
else -> when {
|
||||
mimeType.startsWith("image/") -> R.string.file_type_image
|
||||
mimeType.startsWith("video/") -> R.string.file_type_video
|
||||
mimeType.startsWith("audio/") -> R.string.file_type_music
|
||||
else -> R.string.file_type_none
|
||||
}
|
||||
}
|
||||
if (resource == R.string.file_type_none && label.matches(Regex(".+\\..+"))) {
|
||||
val extension = label.substringAfterLast(".").uppercase(Locale.getDefault())
|
||||
if (extension == "kvaesitso") return context.getString(
|
||||
R.string.file_type_launcherbackup,
|
||||
context.getString(R.string.app_name)
|
||||
)
|
||||
return context.getString(R.string.file_type_generic, extension)
|
||||
}
|
||||
return context.getString(resource)
|
||||
}
|
||||
|
||||
val isDeletable: Boolean
|
||||
get() = false
|
||||
suspend fun delete(context: Context) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class GDriveFile(
|
||||
val fileId: String,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
override val mimeType: String,
|
||||
override val size: Long,
|
||||
override val isDirectory: Boolean,
|
||||
override val metaData: List<Pair<Int, String>>,
|
||||
val directoryColor: String?,
|
||||
val viewUri: String,
|
||||
override val labelOverride: String? = null,
|
||||
) : File {
|
||||
|
||||
override fun overrideLabel(label: String): GDriveFile {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String = "$domain://$fileId"
|
||||
|
||||
override val isStoredInCloud = true
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_gdrive
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
return Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(viewUri)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "gdrive"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.location.Geocoder
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.ThumbnailUtils
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Size
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.ktx.formatToString
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.media.ThumbnailUtilsCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.io.File as JavaIOFile
|
||||
|
||||
data class LocalFile(
|
||||
val id: Long,
|
||||
override val path: String,
|
||||
override val mimeType: String,
|
||||
override val size: Long,
|
||||
override val isDirectory: Boolean,
|
||||
override val metaData: List<Pair<Int, String>>,
|
||||
override val labelOverride: String? = null
|
||||
) : File {
|
||||
|
||||
override val label = path.substringAfterLast('/')
|
||||
|
||||
override fun overrideLabel(label: String): LocalFile {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key = "$domain://$path"
|
||||
|
||||
override val isStoredInCloud = false
|
||||
|
||||
override suspend fun loadIcon(
|
||||
context: Context,
|
||||
size: Int,
|
||||
themed: Boolean,
|
||||
): LauncherIcon? {
|
||||
if (!JavaIOFile(path).exists()) return null
|
||||
when {
|
||||
mimeType.startsWith("image/") -> {
|
||||
val thumbnail = withContext(Dispatchers.IO) {
|
||||
ThumbnailUtils.extractThumbnail(
|
||||
BitmapFactory.decodeFile(path),
|
||||
size, size
|
||||
)
|
||||
} ?: return null
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = BitmapDrawable(context.resources, thumbnail),
|
||||
scale = 1f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
}
|
||||
mimeType.startsWith("video/") -> {
|
||||
val thumbnail = withContext(Dispatchers.IO) {
|
||||
ThumbnailUtilsCompat.createVideoThumbnail(
|
||||
JavaIOFile(path),
|
||||
Size(size, size)
|
||||
)
|
||||
} ?: return null
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = BitmapDrawable(context.resources, thumbnail),
|
||||
scale = 1f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
}
|
||||
mimeType.startsWith("audio/") -> {
|
||||
val thumbnail = withContext(Dispatchers.IO) {
|
||||
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||
try {
|
||||
mediaMetadataRetriever.setDataSource(path)
|
||||
val thumbData = mediaMetadataRetriever.embeddedPicture
|
||||
if (thumbData != null) {
|
||||
val thumbnail = ThumbnailUtils.extractThumbnail(
|
||||
BitmapFactory.decodeByteArray(thumbData, 0, thumbData.size),
|
||||
size,
|
||||
size
|
||||
)
|
||||
mediaMetadataRetriever.release()
|
||||
return@withContext thumbnail
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
}
|
||||
mediaMetadataRetriever.release()
|
||||
return@withContext null
|
||||
|
||||
}
|
||||
thumbnail ?: return null
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = BitmapDrawable(context.resources, thumbnail),
|
||||
scale = 1f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
|
||||
}
|
||||
mimeType == "application/vnd.android.package-archive" -> {
|
||||
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
|
||||
val icon = withContext(Dispatchers.IO) {
|
||||
pkgInfo?.applicationInfo?.loadIcon(context.packageManager)
|
||||
} ?: return null
|
||||
when (icon) {
|
||||
is AdaptiveIconDrawable -> {
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = icon.foreground?.let {
|
||||
StaticIconLayer(
|
||||
icon = it,
|
||||
scale = 1.5f,
|
||||
)
|
||||
} ?: TransparentLayer,
|
||||
backgroundLayer = icon.background?.let {
|
||||
StaticIconLayer(
|
||||
icon = it,
|
||||
scale = 1.5f,
|
||||
)
|
||||
} ?: TransparentLayer,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = icon,
|
||||
scale = 0.7f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
private fun getLaunchIntent(context: Context): Intent {
|
||||
val uri = if (isDirectory) {
|
||||
Uri.parse(path)
|
||||
} else {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
context.applicationContext.packageName + ".fileprovider", JavaIOFile(path)
|
||||
)
|
||||
}
|
||||
return Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(uri, mimeType)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(getLaunchIntent(context), options)
|
||||
}
|
||||
|
||||
override val isDeletable: Boolean
|
||||
get() {
|
||||
val file = java.io.File(path)
|
||||
return file.canWrite() && file.parentFile?.canWrite() == true
|
||||
}
|
||||
|
||||
override suspend fun delete(context: Context) {
|
||||
super.delete(context)
|
||||
|
||||
val file = java.io.File(path)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
file.deleteRecursively()
|
||||
|
||||
context.contentResolver.delete(
|
||||
MediaStore.Files.getContentUri("external"),
|
||||
"${MediaStore.Files.FileColumns._ID} = ?",
|
||||
arrayOf(id.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val Domain = "file"
|
||||
|
||||
internal fun getMimetypeByFileExtension(extension: String): String {
|
||||
return when (extension) {
|
||||
"apk" -> "application/vnd.android.package-archive"
|
||||
"zip" -> "application/zip"
|
||||
"jar" -> "application/java-archive"
|
||||
"txt" -> "text/plain"
|
||||
"js" -> "text/javascript"
|
||||
"html", "htm" -> "text/html"
|
||||
"css" -> "text/css"
|
||||
"gif" -> "image/gif"
|
||||
"png" -> "image/png"
|
||||
"jpg", "jpeg" -> "image/jpeg"
|
||||
"bmp" -> "image/bmp"
|
||||
"webp" -> "image/webp"
|
||||
"ico" -> "image/x-icon"
|
||||
"midi" -> "audio/midi"
|
||||
"mp3" -> "audio/mpeg3"
|
||||
"webm" -> "audio/webm"
|
||||
"ogg" -> "audio/ogg"
|
||||
"wav" -> "audio/wav"
|
||||
"mp4" -> "video/mp4"
|
||||
"kvaesitso" -> "application/vendor.de.mm20.launcher2.backup"
|
||||
else -> "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun getMetaData(
|
||||
context: Context,
|
||||
mimeType: String,
|
||||
path: String
|
||||
): List<Pair<Int, String>> {
|
||||
val metaData = mutableListOf<Pair<Int, String>>()
|
||||
when {
|
||||
mimeType.startsWith("audio/") -> {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(path)
|
||||
arrayOf(
|
||||
R.string.file_meta_title to MediaMetadataRetriever.METADATA_KEY_TITLE,
|
||||
R.string.file_meta_artist to MediaMetadataRetriever.METADATA_KEY_ARTIST,
|
||||
R.string.file_meta_album to MediaMetadataRetriever.METADATA_KEY_ALBUM,
|
||||
R.string.file_meta_year to MediaMetadataRetriever.METADATA_KEY_YEAR
|
||||
).forEach {
|
||||
retriever.extractMetadata(it.second)
|
||||
?.let { m -> metaData.add(it.first to m) }
|
||||
}
|
||||
val duration =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
?.toLong() ?: 0
|
||||
val d = DateUtils.formatElapsedTime((duration) / 1000)
|
||||
metaData.add(3, R.string.file_meta_duration to d)
|
||||
retriever.release()
|
||||
} catch (e: RuntimeException) {
|
||||
retriever.release()
|
||||
}
|
||||
}
|
||||
mimeType.startsWith("video/") -> {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
try {
|
||||
retriever.setDataSource(path)
|
||||
val width =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
||||
?.toLong() ?: 0
|
||||
val height =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
||||
?.toLong() ?: 0
|
||||
metaData.add(R.string.file_meta_dimensions to "${width}x$height")
|
||||
val duration =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
?.toLong() ?: 0
|
||||
val d = DateUtils.formatElapsedTime(duration / 1000)
|
||||
metaData.add(R.string.file_meta_duration to d)
|
||||
val loc =
|
||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
|
||||
if (Geocoder.isPresent() && loc != null) {
|
||||
val lon =
|
||||
loc.substring(0, loc.lastIndexOfAny(charArrayOf('+', '-')))
|
||||
.toDouble()
|
||||
val lat = loc.substring(
|
||||
loc.lastIndexOfAny(charArrayOf('+', '-')),
|
||||
loc.indexOf('/')
|
||||
).toDouble()
|
||||
val list = Geocoder(context).getFromLocation(lon, lat, 1)
|
||||
if (list != null && list.size > 0) {
|
||||
metaData.add(R.string.file_meta_location to list[0].formatToString())
|
||||
}
|
||||
}
|
||||
retriever.release()
|
||||
} catch (e: RuntimeException) {
|
||||
retriever.release()
|
||||
}
|
||||
}
|
||||
mimeType.startsWith("image/") -> {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(path, options)
|
||||
val width = options.outWidth
|
||||
val height = options.outHeight
|
||||
metaData.add(R.string.file_meta_dimensions to "${width}x$height")
|
||||
try {
|
||||
val exif = ExifInterface(path)
|
||||
val loc = exif.latLong
|
||||
if (loc != null && Geocoder.isPresent()) {
|
||||
val list = Geocoder(context).getFromLocation(loc[0], loc[1], 1)
|
||||
if (list != null && list.size > 0) {
|
||||
metaData.add(R.string.file_meta_location to list[0].formatToString())
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
|
||||
}
|
||||
}
|
||||
mimeType == "application/vnd.android.package-archive" -> {
|
||||
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
|
||||
?: return metaData
|
||||
metaData.add(
|
||||
R.string.file_meta_app_name to pkgInfo.applicationInfo.loadLabel(
|
||||
context.packageManager
|
||||
).toString()
|
||||
)
|
||||
metaData.add(R.string.file_meta_app_pkgname to pkgInfo.packageName)
|
||||
metaData.add(R.string.file_meta_app_version to pkgInfo.versionName)
|
||||
metaData.add(R.string.file_meta_app_min_sdk to pkgInfo.applicationInfo.minSdkVersion.toString())
|
||||
}
|
||||
}
|
||||
return metaData
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class NextcloudFile(
|
||||
val fileId: Long,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
override val mimeType: String,
|
||||
override val size: Long,
|
||||
override val isDirectory: Boolean,
|
||||
val server: String,
|
||||
override val metaData: List<Pair<Int, String>>,
|
||||
override val labelOverride: String? = null,
|
||||
) : File {
|
||||
|
||||
override fun overrideLabel(label: String): NextcloudFile {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String = "$domain://$server/$fileId"
|
||||
|
||||
override val isStoredInCloud: Boolean
|
||||
get() = true
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_nextcloud
|
||||
|
||||
private fun getLaunchIntent(context: Context): Intent {
|
||||
return Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("$server/f/$fileId")
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
`package` = getNextcloudAppPackage(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(getLaunchIntent(context), options)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val Domain = "nextcloud"
|
||||
private fun getNextcloudAppPackage(context: Context): String? {
|
||||
val candidates = listOf("com.nextcloud.client", "com.nextcloud.android.beta")
|
||||
|
||||
for (c in candidates) {
|
||||
try {
|
||||
context.packageManager.getPackageInfo(c, 0)
|
||||
return c
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class OneDriveFile(
|
||||
val fileId: String,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
override val mimeType: String,
|
||||
override val size: Long,
|
||||
override val isDirectory: Boolean,
|
||||
override val metaData: List<Pair<Int, String>>,
|
||||
val webUrl: String,
|
||||
override val labelOverride: String? = null,
|
||||
) : File {
|
||||
|
||||
override fun overrideLabel(label: String): OneDriveFile {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String = "$domain://$fileId"
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_onedrive
|
||||
|
||||
override val isStoredInCloud = true
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
return Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(webUrl)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "onedrive"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class OwncloudFile(
|
||||
val fileId: Long,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
override val mimeType: String,
|
||||
override val size: Long,
|
||||
override val isDirectory: Boolean,
|
||||
val server: String,
|
||||
override val metaData: List<Pair<Int, String>>,
|
||||
override val labelOverride: String? = null,
|
||||
) : File {
|
||||
|
||||
override fun overrideLabel(label: String): OwncloudFile {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String = "$domain://$server/$fileId"
|
||||
|
||||
override val isStoredInCloud: Boolean
|
||||
get() = true
|
||||
|
||||
override val providerIconRes = R.drawable.ic_badge_owncloud
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
return Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("$server/f/$fileId")
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
}
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "owncloud"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user