Initial commit

This commit is contained in:
MM20
2021-09-18 23:37:52 +02:00
commit 749e4e3073
938 changed files with 50475 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+57
View File
@@ -0,0 +1,57 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.exifinterface)
implementation(libs.bundles.androidx.lifecycle)
implementation(project(":search"))
implementation(project(":hiddenitems"))
implementation(project(":preferences"))
implementation(project(":base"))
implementation(project(":ktx"))
implementation(project(":ms-services"))
implementation(project(":g-services"))
implementation(project(":nextcloud"))
implementation(project(":owncloud"))
implementation(project(":i18n"))
implementation(project(":permissions"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+5
View File
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.files">
/
</manifest>
@@ -0,0 +1,70 @@
package de.mm20.launcher2.files
import android.content.Context
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.*
import kotlinx.coroutines.*
class FilesRepository private constructor(val context: Context) : BaseSearchableRepository() {
val files = MediatorLiveData<List<File>?>()
private val allFiles = MutableLiveData<List<File>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
private val nextcloudClient by lazy {
NextcloudApiHelper(context)
}
private val owncloudClient by lazy {
OwncloudClient(context)
}
init {
files.addSource(hiddenItemKeys) { keys ->
files.value = allFiles.value?.filter { !keys.contains(it.key) }
}
files.addSource(allFiles) { f ->
files.value = f?.filter { hiddenItemKeys.value?.contains(it.key) != true }
}
}
override suspend fun search(query: String) {
if (query.isBlank()) {
allFiles.value = null
return
}
val localFiles = withContext(Dispatchers.IO) {
File.search(context, query).sorted().toMutableList()
}
allFiles.value = localFiles
val cloudFiles = withContext(Dispatchers.IO) {
delay(300)
listOf(
async { OneDriveFile.search(context, query) },
async { GDriveFile.search(context, query) },
async { NextcloudFile.search(context, query, nextcloudClient) },
async { OwncloudFile.search(context, query, owncloudClient) }
).awaitAll().flatten()
}
yield()
allFiles.value = localFiles + cloudFiles
}
fun removeFile(file: File) {
allFiles.value = allFiles.value?.filter { it != file }
}
companion object {
private lateinit var instance: FilesRepository
fun getInstance(context: Context): FilesRepository {
if (!::instance.isInitialized) instance = FilesRepository(context.applicationContext)
return instance
}
}
}
@@ -0,0 +1,16 @@
package de.mm20.launcher2.files
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import de.mm20.launcher2.search.data.File
class FilesViewModel(app: Application): AndroidViewModel(app) {
private val repository = FilesRepository.getInstance(app)
val files = repository.files
fun removeFile(file: File) {
repository.removeFile(file)
}
}
@@ -0,0 +1,26 @@
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 androidx.core.content.ContentResolverCompat
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,403 @@
package de.mm20.launcher2.search.data
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.graphics.BitmapFactory
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.location.Geocoder
import android.media.MediaMetadataRetriever
import android.media.ThumbnailUtils
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.text.format.DateUtils
import android.util.Size
import androidx.core.content.ContentResolverCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.database.getStringOrNull
import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
import java.io.IOException
import java.util.*
import java.io.File as JavaIOFile
open class File(
val id: Long,
val path: String,
val mimeType: String,
val size: Long,
val isDirectory: Boolean,
val metaData: List<Pair<Int, String>>
) : Searchable() {
override val label = path.substringAfterLast('/')
override val key = "file://$path"
open val isStoredInCloud = false
override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? {
if (!JavaIOFile(path).exists()) return null
when {
mimeType.startsWith("image/") -> {
val thumbnail = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(path),
size, size) ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType.startsWith("video/") -> {
val thumbnail = ThumbnailUtilsCompat.createVideoThumbnail(JavaIOFile(path),
Size(size, size)) ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
mimeType.startsWith("audio/") -> {
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()
thumbnail ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
} catch (e: RuntimeException) {
mediaMetadataRetriever.release()
return null
}
}
mimeType == "application/vnd.android.package-archive" -> {
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
val icon = pkgInfo?.applicationInfo?.loadIcon(context.packageManager) ?: return null
when {
Build.VERSION.SDK_INT > Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> {
return LauncherIcon(
foreground = icon.foreground,
background = icon.background,
foregroundScale = 1.5f,
backgroundScale = 1.5f
)
}
else -> {
return LauncherIcon(
foreground = icon,
foregroundScale = 0.7f,
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
}
}
}
}
return null
}
override fun getPlaceholderIcon(context: Context): LauncherIcon {
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 LauncherIcon(
foreground = context.getDrawable(resId)!!,
background = ColorDrawable(ContextCompat.getColor(context, bgColor)),
foregroundScale = 0.5f
)
}
override fun getLaunchIntent(context: Context): Intent? {
val uri = 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 serialize(): String {
return jsonObjectOf(
"id" to id
).toString()
}
fun getFileType(context: Context): String {
if (isDirectory) return context.getString(R.string.file_type_directory)
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(".").toUpperCase(Locale.getDefault())
return context.getString(R.string.file_type_generic, extension)
}
return context.getString(resource)
}
companion object {
fun search(context: Context, query: String): List<File> {
val results = mutableListOf<File>()
if (!LauncherPreferences.instance.searchFiles) return results
if (query.isBlank()) return results
if (!PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) 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 = File(
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 }
}
private 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"
else -> "application/octet-stream"
}
}
private 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.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.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)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
metaData.add(R.string.file_meta_app_min_sdk to pkgInfo.applicationInfo.minSdkVersion.toString())
}
}
}
return metaData
}
fun deserialize(context: Context, serialized: String): File? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) 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 (!JavaIOFile(path).exists()) return null
val directory = JavaIOFile(path).isDirectory
val id = cursor.getLong(0)
val mimeType = cursor.getStringOrNull(3)
?: if (directory) "inode/directory" else getMimetypeByFileExtension(path.substringAfterLast('.'))
val size = cursor.getLong(1)
cursor.close()
return File(
path = path,
mimeType = mimeType,
size = size,
isDirectory = directory,
id = id,
metaData = getMetaData(context, mimeType, path))
}
cursor.close()
return null
}
}
}
@@ -0,0 +1,126 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.mm20.launcher2.files.R
import de.mm20.launcher2.gservices.DriveFileMeta
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class GDriveFile(
val fileId: String,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>,
val directoryColor: String?,
val viewUri: String
) : File(0, path, mimeType, size, isDirectory, metaData) {
override val key: String = "gdrive://$fileId"
override val badgeKey: String
get() = "gdrive://"
override fun serialize(): String {
return jsonObjectOf(
"id" to fileId,
"label" to label,
"path" to path,
"mimeType" to mimeType,
"size" to size,
"directory" to isDirectory,
"color" to directoryColor,
"uri" to viewUri
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
else -> "other"
}, v)
}
}.toString()
}
override val isStoredInCloud = true
override fun getLaunchIntent(context: Context): Intent? {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(viewUri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override suspend fun loadIconAsync(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
}
fun deserialize(serialized: String): GDriveFile? {
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
)
}
}
}
@@ -0,0 +1,101 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.mm20.launcher2.files.R
import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class NextcloudFile(
fileId: Long,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
val server: String,
metaData: List<Pair<Int, String>>
) : File(fileId, path, mimeType, size, isDirectory, metaData) {
override val badgeKey: String = "nextcloud://"
override val key: String = "nextcloud://$server/$fileId"
override val isStoredInCloud: Boolean
get() = true
override fun getLaunchIntent(context: Context): Intent? {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("$server/f/$id")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override fun serialize(): String {
return jsonObjectOf(
"id" to id,
"label" to label,
"path" to path,
"mimeType" to mimeType,
"size" to size,
"isDirectory" to isDirectory,
"server" to server
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
else -> "other"
}, v)
}
}.toString()
}
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()
)
}
}
fun deserialize(serialized: String): NextcloudFile? {
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()
)
}
}
}
@@ -0,0 +1,123 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.mm20.launcher2.msservices.DriveItem
import de.mm20.launcher2.files.R
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class OneDriveFile(
val fileId: String,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>,
val webUrl: String
) : File(0, path, mimeType, size, isDirectory, metaData) {
override val badgeKey: String = "onedrive://"
override val key: String = "onedrive://$fileId"
override val isStoredInCloud = true
override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? {
return null
}
override fun getLaunchIntent(context: Context): Intent? {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(webUrl)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override fun serialize(): String {
return jsonObjectOf(
"id" to fileId,
"label" to label,
"mimeType" to mimeType,
"size" to size,
"directory" to isDirectory,
"webUrl" to webUrl
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
else -> "other"
}, v)
}
}.toString()
}
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()
}
fun deserialize(serialized: String): OneDriveFile? {
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
)
}
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,101 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.mm20.launcher2.files.R
import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class OwncloudFile(
fileId: Long,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
val server: String,
metaData: List<Pair<Int, String>>
) : File(fileId, path, mimeType, size, isDirectory, metaData) {
override val badgeKey: String = "owncloud://"
override val key: String = "owncloud://$server/$fileId"
override val isStoredInCloud: Boolean
get() = true
override fun getLaunchIntent(context: Context): Intent? {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("$server/f/$id")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override fun serialize(): String {
return jsonObjectOf(
"id" to id,
"label" to label,
"path" to path,
"mimeType" to mimeType,
"size" to size,
"isDirectory" to isDirectory,
"server" to server
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
else -> "other"
}, v)
}
}.toString()
}
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()
)
}
}
fun deserialize(serialized: String): OwncloudFile? {
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,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M14,17H12V15H10V13H12V15H14M14,9H12V11H14V13H12V11H10V9H12V7H10V5H12V7H14M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M14,17H7V15H14M17,13H7V11H17M17,9H7V7H17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</vector>
@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#FFF" android:pathData="M9,5V9H21V5M9,19H21V15H9M9,14H21V10H9M4,9H8V5H4M4,19H8V15H4M4,14H8V10H4V14Z" />
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M11.43,10.94C11.2,11.68 10.87,12.47 10.42,13.34C10.22,13.72 10,14.08 9.92,14.38L10.03,14.34V14.34C11.3,13.85 12.5,13.57 13.37,13.41C13.22,13.31 13.08,13.2 12.96,13.09C12.36,12.58 11.84,11.84 11.43,10.94M17.91,14.75C17.74,14.94 17.44,15.05 17,15.05C16.24,15.05 15,14.82 14,14.31C12.28,14.5 11,14.73 9.97,15.06C9.92,15.08 9.86,15.1 9.79,15.13C8.55,17.25 7.63,18.2 6.82,18.2C6.66,18.2 6.5,18.16 6.38,18.09L5.9,17.78L5.87,17.73C5.8,17.55 5.78,17.38 5.82,17.19C5.93,16.66 6.5,15.82 7.7,15.07C7.89,14.93 8.19,14.77 8.59,14.58C8.89,14.06 9.21,13.45 9.55,12.78C10.06,11.75 10.38,10.73 10.63,9.85V9.84C10.26,8.63 10.04,7.9 10.41,6.57C10.5,6.19 10.83,5.8 11.2,5.8H11.44C11.67,5.8 11.89,5.88 12.05,6.04C12.71,6.7 12.4,8.31 12.07,9.64C12.05,9.7 12.04,9.75 12.03,9.78C12.43,10.91 13,11.82 13.63,12.34C13.89,12.54 14.18,12.74 14.5,12.92C14.95,12.87 15.38,12.85 15.79,12.85C17.03,12.85 17.78,13.07 18.07,13.54C18.17,13.7 18.22,13.89 18.19,14.09C18.18,14.34 18.09,14.57 17.91,14.75M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M17.5,14.04C17.4,13.94 17,13.69 15.6,13.69C15.53,13.69 15.46,13.69 15.37,13.79C16.1,14.11 16.81,14.3 17.27,14.3C17.34,14.3 17.4,14.29 17.46,14.28H17.5C17.55,14.26 17.58,14.25 17.59,14.15C17.57,14.12 17.55,14.08 17.5,14.04M8.33,15.5C8.12,15.62 7.95,15.73 7.85,15.81C7.14,16.46 6.69,17.12 6.64,17.5C7.09,17.35 7.68,16.69 8.33,15.5M11.35,8.59L11.4,8.55C11.47,8.23 11.5,7.95 11.56,7.73L11.59,7.57C11.69,7 11.67,6.71 11.5,6.47L11.35,6.42C11.33,6.45 11.3,6.5 11.28,6.54C11.11,6.96 11.12,7.69 11.35,8.59Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M19,16H5V8H19M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M4,3H20A2,2 0 0,1 22,5V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V5A2,2 0 0,1 4,3M4,7V10H8V7H4M10,7V10H14V7H10M20,10V7H16V10H20M4,12V15H8V12H4M4,20H8V17H4V20M10,12V15H14V12H10M10,20H14V17H10V20M20,20V17H16V20H20M20,12H16V15H20V12Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M18,3v2h-2L16,3L8,3v2L6,5L6,3L4,3v18h2v-2h2v2h8v-2h2v2h2L20,3h-2zM8,17L6,17v-2h2v2zM8,13L6,13v-2h2v2zM8,9L6,9L6,7h2v2zM18,17h-2v-2h2v2zM18,13h-2v-2h2v2zM18,9h-2L16,7h2v2z"/>
</vector>