more idiomatic kotlin, removed guava

This commit is contained in:
Thibault Deckers 2020-10-29 11:49:18 +09:00
parent bb96f2f65a
commit 7aa50e7880
7 changed files with 74 additions and 126 deletions

View file

@ -100,7 +100,6 @@ dependencies {
implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.google.guava:guava:30.0-android'
kapt 'androidx.annotation:annotation:1.1.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'

View file

@ -11,6 +11,7 @@ import deckers.thibault.aves.model.provider.MediaStoreImageProvider
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@ -27,9 +28,9 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
GlobalScope.launch { Glide.get(activity).clearDiskCache() }
result.success(null)
}
"rename" -> GlobalScope.launch { rename(call, Coresult(result)) }
"rotate" -> GlobalScope.launch { rotate(call, Coresult(result)) }
"flip" -> GlobalScope.launch { flip(call, Coresult(result)) }
"rename" -> GlobalScope.launch(Dispatchers.IO) { rename(call, Coresult(result)) }
"rotate" -> GlobalScope.launch(Dispatchers.IO) { rotate(call, Coresult(result)) }
"flip" -> GlobalScope.launch(Dispatchers.IO) { flip(call, Coresult(result)) }
else -> result.notImplemented()
}
}

View file

@ -16,7 +16,6 @@ import com.adobe.internal.xmp.XMPUtils
import com.adobe.internal.xmp.properties.XMPPropertyInfo
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.imaging.ImageMetadataReader
import com.drew.imaging.ImageProcessingException
import com.drew.lang.Rational
import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
@ -59,7 +58,6 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
import kotlin.math.roundToLong
@ -568,9 +566,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
}
} catch (e: IOException) {
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e)
} catch (e: ImageProcessingException) {
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e)
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e)

View file

@ -13,10 +13,10 @@ import deckers.thibault.aves.utils.LogUtils.createTag
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.ExecutionException
class ImageOpStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler {
private lateinit var eventSink: EventSink
@ -41,8 +41,8 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
handler = Handler(Looper.getMainLooper())
when (op) {
"delete" -> GlobalScope.launch { delete() }
"move" -> GlobalScope.launch { move() }
"delete" -> GlobalScope.launch(Dispatchers.IO) { delete() }
"move" -> GlobalScope.launch(Dispatchers.IO) { move() }
else -> endOfStream()
}
}
@ -92,7 +92,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
endOfStream()
}
private fun delete() {
private suspend fun delete() {
if (entryMapList.isEmpty()) {
endOfStream()
return
@ -114,12 +114,9 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
"uri" to uri.toString(),
)
try {
provider.delete(context, uri, path).get()
provider.delete(context, uri, path)
result["success"] = true
} catch (e: ExecutionException) {
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
result["success"] = false
} catch (e: InterruptedException) {
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
result["success"] = false
}

View file

@ -8,8 +8,6 @@ import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.commonsware.cwac.document.DocumentFileCompat
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import deckers.thibault.aves.model.AvesImageEntry
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.utils.LogUtils.createTag
@ -30,8 +28,8 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException())
}
open fun delete(context: Context, uri: Uri, path: String?): ListenableFuture<Any?> {
return Futures.immediateFailedFuture(UnsupportedOperationException())
open suspend fun delete(context: Context, uri: Uri, path: String?) {
throw UnsupportedOperationException()
}
open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List<AvesImageEntry>, callback: ImageOpCallback) {
@ -49,6 +47,7 @@ abstract class ImageProvider {
val df = getDocumentFile(context, oldPath, oldMediaUri)
try {
@Suppress("BlockingMethodInNonBlockingContext")
val renamed = df != null && df.renameTo(newFilename)
if (!renamed) {
callback.onFailure(Exception("failed to rename entry at path=$oldPath"))

View file

@ -8,8 +8,6 @@ import android.os.Build
import android.provider.MediaStore
import android.util.Log
import com.commonsware.cwac.document.DocumentFileCompat
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import deckers.thibault.aves.model.AvesImageEntry
import deckers.thibault.aves.model.SourceImageEntry
import deckers.thibault.aves.utils.LogUtils.createTag
@ -22,9 +20,7 @@ import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission
import kotlinx.coroutines.delay
import java.io.File
import java.io.FileNotFoundException
import java.util.*
import java.util.concurrent.ExecutionException
class MediaStoreImageProvider : ImageProvider() {
suspend fun fetchAll(context: Context, knownEntries: Map<Int, Int?>, handleNewEntry: NewEntryHandler) {
@ -176,41 +172,21 @@ class MediaStoreImageProvider : ImageProvider() {
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
// `uri` is a media URI, not a document URI
override fun delete(context: Context, uri: Uri, path: String?): ListenableFuture<Any?> {
val future = SettableFuture.create<Any?>()
if (path == null) {
future.setException(Exception("failed to delete file because path is null"))
return future
}
override suspend fun delete(context: Context, uri: Uri, path: String?) {
path ?: throw Exception("failed to delete file because path is null")
if (requireAccessPermission(context, path)) {
// if the file is on SD card, calling the content resolver `delete()` removes the entry from the Media Store
// but it doesn't delete the file, even if the app has the permission
try {
val df = getDocumentFile(context, path, uri)
if (df != null && df.delete()) {
future.set(null)
} else {
future.setException(Exception("failed to delete file with df=$df"))
}
} catch (e: FileNotFoundException) {
future.setException(e)
}
return future
val df = getDocumentFile(context, path, uri)
@Suppress("BlockingMethodInNonBlockingContext")
if (df != null && df.delete()) return
throw Exception("failed to delete file with df=$df")
}
try {
if (context.contentResolver.delete(uri, null, null) > 0) {
future.set(null)
} else {
future.setException(Exception("failed to delete row from content provider"))
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to delete entry", e)
future.setException(e)
}
return future
if (context.contentResolver.delete(uri, null, null) > 0) return
throw Exception("failed to delete row from content provider")
}
override suspend fun moveMultiple(
@ -252,14 +228,12 @@ class MediaStoreImageProvider : ImageProvider() {
// - there is no documentation regarding support for usage with removable storage
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try {
val newFieldsFuture = moveSingleByTreeDocAndScan(
val newFields = moveSingleByTreeDocAndScan(
context, sourcePath, sourceUri, destinationDir, destinationDirDocFile, mimeType, copy,
)
result["newFields"] = newFieldsFuture.get()
result["newFields"] = newFields
result["success"] = true
} catch (e: ExecutionException) {
Log.w(LOG_TAG, "failed to move to destinationDir=$destinationDir entry with sourcePath=$sourcePath", e)
} catch (e: InterruptedException) {
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to move to destinationDir=$destinationDir entry with sourcePath=$sourcePath", e)
}
}
@ -275,68 +249,56 @@ class MediaStoreImageProvider : ImageProvider() {
destinationDirDocFile: DocumentFileCompat,
mimeType: String,
copy: Boolean,
): ListenableFuture<FieldMap> {
val future = SettableFuture.create<FieldMap>()
try {
val sourceFile = File(sourcePath)
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == destinationDir) {
if (copy) {
future.setException(Exception("file at path=$sourcePath is already in destination directory"))
} else {
future.set(HashMap<String, Any?>())
}
} else {
val sourceFileName = sourceFile.name
val desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "")
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension)
val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri)
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
// when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri`
val source = DocumentFileCompat.fromSingleUri(context, sourceUri)
source.copyTo(destinationDocFile)
// the source file name and the created document file name can be different when:
// - a file with the same name already exists, so the name gets a suffix like ` (1)`
// - the original extension does not match the extension added by the underlying provider
val fileName = destinationDocFile.name
val destinationFullPath = destinationDir + fileName
var deletedSource = false
if (!copy) {
// delete original entry
try {
delete(context, sourceUri, sourcePath).get()
deletedSource = true
} catch (e: ExecutionException) {
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
} catch (e: InterruptedException) {
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
}
}
try {
val fields = scanNewPath(context, destinationFullPath, mimeType)
fields["deletedSource"] = deletedSource
future.set(fields)
} catch (e: Exception) {
future.setException(e)
}
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to ${(if (copy) "copy" else "move")} entry", e)
future.setException(e)
): FieldMap {
val sourceFile = File(sourcePath)
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == destinationDir) {
if (copy) throw Exception("file at path=$sourcePath is already in destination directory")
return HashMap<String, Any?>()
}
return future
val sourceFileName = sourceFile.name
val desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "")
if (File(destinationDir, sourceFileName).exists()) {
throw Exception("file with name=$sourceFileName already exists in destination directory")
}
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
@Suppress("BlockingMethodInNonBlockingContext")
val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension)
val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri)
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
// when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri`
val source = DocumentFileCompat.fromSingleUri(context, sourceUri)
@Suppress("BlockingMethodInNonBlockingContext")
source.copyTo(destinationDocFile)
// the source file name and the created document file name can be different when:
// - a file with the same name already exists, some implementations give a suffix like ` (1)`, some *do not*
// - the original extension does not match the extension added by the underlying provider
val fileName = destinationDocFile.name
val destinationFullPath = destinationDir + fileName
var deletedSource = false
if (!copy) {
// delete original entry
try {
delete(context, sourceUri, sourcePath)
deletedSource = true
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
}
}
return scanNewPath(context, destinationFullPath, mimeType).apply {
put("deletedSource", deletedSource)
}
}
companion object {

View file

@ -37,12 +37,6 @@ class Constants {
licenseUrl: 'https://github.com/bumptech/glide/blob/master/LICENSE',
sourceUrl: 'https://github.com/bumptech/glide',
),
Dependency(
name: 'Guava',
license: 'Apache 2.0',
licenseUrl: 'https://github.com/google/guava/blob/master/COPYING',
sourceUrl: 'https://github.com/google/guava',
),
Dependency(
name: 'Metadata Extractor',
license: 'Apache 2.0',