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.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0' implementation 'com.drewnoakes:metadata-extractor:2.15.0'
implementation 'com.github.bumptech.glide:glide:4.11.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 'androidx.annotation:annotation:1.1.0'
kapt 'com.github.bumptech.glide:compiler:4.11.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.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -27,9 +28,9 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
GlobalScope.launch { Glide.get(activity).clearDiskCache() } GlobalScope.launch { Glide.get(activity).clearDiskCache() }
result.success(null) result.success(null)
} }
"rename" -> GlobalScope.launch { rename(call, Coresult(result)) } "rename" -> GlobalScope.launch(Dispatchers.IO) { rename(call, Coresult(result)) }
"rotate" -> GlobalScope.launch { rotate(call, Coresult(result)) } "rotate" -> GlobalScope.launch(Dispatchers.IO) { rotate(call, Coresult(result)) }
"flip" -> GlobalScope.launch { flip(call, Coresult(result)) } "flip" -> GlobalScope.launch(Dispatchers.IO) { flip(call, Coresult(result)) }
else -> result.notImplemented() else -> result.notImplemented()
} }
} }

View file

@ -16,7 +16,6 @@ import com.adobe.internal.xmp.XMPUtils
import com.adobe.internal.xmp.properties.XMPPropertyInfo import com.adobe.internal.xmp.properties.XMPPropertyInfo
import com.bumptech.glide.load.resource.bitmap.TransformationUtils import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.imaging.ImageMetadataReader import com.drew.imaging.ImageMetadataReader
import com.drew.imaging.ImageProcessingException
import com.drew.lang.Rational import com.drew.lang.Rational
import com.drew.metadata.exif.ExifDirectoryBase import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.exif.ExifIFD0Directory
@ -59,7 +58,6 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.* import java.util.*
import kotlin.math.roundToLong import kotlin.math.roundToLong
@ -568,9 +566,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
} }
} }
} }
} catch (e: IOException) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e)
} catch (e: ImageProcessingException) {
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e) Log.w(LOG_TAG, "failed to extract xmp thumbnail", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e) 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 deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink import io.flutter.plugin.common.EventChannel.EventSink
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException
class ImageOpStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler { class ImageOpStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler {
private lateinit var eventSink: EventSink private lateinit var eventSink: EventSink
@ -41,8 +41,8 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
handler = Handler(Looper.getMainLooper()) handler = Handler(Looper.getMainLooper())
when (op) { when (op) {
"delete" -> GlobalScope.launch { delete() } "delete" -> GlobalScope.launch(Dispatchers.IO) { delete() }
"move" -> GlobalScope.launch { move() } "move" -> GlobalScope.launch(Dispatchers.IO) { move() }
else -> endOfStream() else -> endOfStream()
} }
} }
@ -92,7 +92,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
endOfStream() endOfStream()
} }
private fun delete() { private suspend fun delete() {
if (entryMapList.isEmpty()) { if (entryMapList.isEmpty()) {
endOfStream() endOfStream()
return return
@ -114,12 +114,9 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
"uri" to uri.toString(), "uri" to uri.toString(),
) )
try { try {
provider.delete(context, uri, path).get() provider.delete(context, uri, path)
result["success"] = true result["success"] = true
} catch (e: ExecutionException) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
result["success"] = false
} catch (e: InterruptedException) {
Log.w(LOG_TAG, "failed to delete entry with path=$path", e) Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
result["success"] = false result["success"] = false
} }

View file

@ -8,8 +8,6 @@ import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.commonsware.cwac.document.DocumentFileCompat 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.AvesImageEntry
import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.utils.LogUtils.createTag import deckers.thibault.aves.utils.LogUtils.createTag
@ -30,8 +28,8 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException()) callback.onFailure(UnsupportedOperationException())
} }
open fun delete(context: Context, uri: Uri, path: String?): ListenableFuture<Any?> { open suspend fun delete(context: Context, uri: Uri, path: String?) {
return Futures.immediateFailedFuture(UnsupportedOperationException()) throw UnsupportedOperationException()
} }
open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List<AvesImageEntry>, callback: ImageOpCallback) { 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) val df = getDocumentFile(context, oldPath, oldMediaUri)
try { try {
@Suppress("BlockingMethodInNonBlockingContext")
val renamed = df != null && df.renameTo(newFilename) val renamed = df != null && df.renameTo(newFilename)
if (!renamed) { if (!renamed) {
callback.onFailure(Exception("failed to rename entry at path=$oldPath")) 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.provider.MediaStore
import android.util.Log import android.util.Log
import com.commonsware.cwac.document.DocumentFileCompat 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.AvesImageEntry
import deckers.thibault.aves.model.SourceImageEntry import deckers.thibault.aves.model.SourceImageEntry
import deckers.thibault.aves.utils.LogUtils.createTag 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 deckers.thibault.aves.utils.StorageUtils.requireAccessPermission
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException
class MediaStoreImageProvider : ImageProvider() { class MediaStoreImageProvider : ImageProvider() {
suspend fun fetchAll(context: Context, knownEntries: Map<Int, Int?>, handleNewEntry: NewEntryHandler) { 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 private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
// `uri` is a media URI, not a document URI // `uri` is a media URI, not a document URI
override fun delete(context: Context, uri: Uri, path: String?): ListenableFuture<Any?> { override suspend fun delete(context: Context, uri: Uri, path: String?) {
val future = SettableFuture.create<Any?>() path ?: throw Exception("failed to delete file because path is null")
if (path == null) {
future.setException(Exception("failed to delete file because path is null"))
return future
}
if (requireAccessPermission(context, path)) { if (requireAccessPermission(context, path)) {
// if the file is on SD card, calling the content resolver `delete()` removes the entry from the Media Store // 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 // but it doesn't delete the file, even if the app has the permission
try {
val df = getDocumentFile(context, path, uri) val df = getDocumentFile(context, path, uri)
if (df != null && df.delete()) {
future.set(null) @Suppress("BlockingMethodInNonBlockingContext")
} else { if (df != null && df.delete()) return
future.setException(Exception("failed to delete file with df=$df")) throw Exception("failed to delete file with df=$df")
}
} catch (e: FileNotFoundException) {
future.setException(e)
}
return future
} }
try { if (context.contentResolver.delete(uri, null, null) > 0) return
if (context.contentResolver.delete(uri, null, null) > 0) { throw Exception("failed to delete row from content provider")
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
} }
override suspend fun moveMultiple( override suspend fun moveMultiple(
@ -252,14 +228,12 @@ class MediaStoreImageProvider : ImageProvider() {
// - there is no documentation regarding support for usage with removable storage // - 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 // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try { try {
val newFieldsFuture = moveSingleByTreeDocAndScan( val newFields = moveSingleByTreeDocAndScan(
context, sourcePath, sourceUri, destinationDir, destinationDirDocFile, mimeType, copy, context, sourcePath, sourceUri, destinationDir, destinationDirDocFile, mimeType, copy,
) )
result["newFields"] = newFieldsFuture.get() result["newFields"] = newFields
result["success"] = true result["success"] = true
} catch (e: ExecutionException) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to move to destinationDir=$destinationDir entry with sourcePath=$sourcePath", e)
} catch (e: InterruptedException) {
Log.w(LOG_TAG, "failed to move to destinationDir=$destinationDir entry with sourcePath=$sourcePath", e) Log.w(LOG_TAG, "failed to move to destinationDir=$destinationDir entry with sourcePath=$sourcePath", e)
} }
} }
@ -275,26 +249,26 @@ class MediaStoreImageProvider : ImageProvider() {
destinationDirDocFile: DocumentFileCompat, destinationDirDocFile: DocumentFileCompat,
mimeType: String, mimeType: String,
copy: Boolean, copy: Boolean,
): ListenableFuture<FieldMap> { ): FieldMap {
val future = SettableFuture.create<FieldMap>()
try {
val sourceFile = File(sourcePath) val sourceFile = File(sourcePath)
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == destinationDir) { if (sourceDir == destinationDir) {
if (copy) { if (copy) throw Exception("file at path=$sourcePath is already in destination directory")
future.setException(Exception("file at path=$sourcePath is already in destination directory")) return HashMap<String, Any?>()
} else {
future.set(HashMap<String, Any?>())
} }
} else {
val sourceFileName = sourceFile.name val sourceFileName = sourceFile.name
val desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") 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` // 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` // but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI // through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
@Suppress("BlockingMethodInNonBlockingContext")
val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension) val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension)
val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri)
@ -302,10 +276,11 @@ class MediaStoreImageProvider : ImageProvider() {
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument" // `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
// when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri` // when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri`
val source = DocumentFileCompat.fromSingleUri(context, sourceUri) val source = DocumentFileCompat.fromSingleUri(context, sourceUri)
@Suppress("BlockingMethodInNonBlockingContext")
source.copyTo(destinationDocFile) source.copyTo(destinationDocFile)
// the source file name and the created document file name can be different when: // 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)` // - 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 // - the original extension does not match the extension added by the underlying provider
val fileName = destinationDocFile.name val fileName = destinationDocFile.name
val destinationFullPath = destinationDir + fileName val destinationFullPath = destinationDir + fileName
@ -314,30 +289,17 @@ class MediaStoreImageProvider : ImageProvider() {
if (!copy) { if (!copy) {
// delete original entry // delete original entry
try { try {
delete(context, sourceUri, sourcePath).get() delete(context, sourceUri, sourcePath)
deletedSource = true deletedSource = true
} catch (e: ExecutionException) { } catch (e: Exception) {
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) Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
} }
} }
try { return scanNewPath(context, destinationFullPath, mimeType).apply {
val fields = scanNewPath(context, destinationFullPath, mimeType) put("deletedSource", deletedSource)
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)
}
return future
}
companion object { companion object {
private val LOG_TAG = createTag(MediaStoreImageProvider::class.java) private val LOG_TAG = createTag(MediaStoreImageProvider::class.java)

View file

@ -37,12 +37,6 @@ class Constants {
licenseUrl: 'https://github.com/bumptech/glide/blob/master/LICENSE', licenseUrl: 'https://github.com/bumptech/glide/blob/master/LICENSE',
sourceUrl: 'https://github.com/bumptech/glide', 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( Dependency(
name: 'Metadata Extractor', name: 'Metadata Extractor',
license: 'Apache 2.0', license: 'Apache 2.0',