diff --git a/app/lib/widget/handler/enhance_handler.dart b/app/lib/widget/handler/enhance_handler.dart index 3b712b58..420ae6f5 100644 --- a/app/lib/widget/handler/enhance_handler.dart +++ b/app/lib/widget/handler/enhance_handler.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api.dart'; -import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/app_localizations.dart'; -import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/help_utils.dart'; @@ -64,10 +62,15 @@ class EnhanceHandler { return; } _log.info("[call] Selected: ${selected.name}"); - final imageUri = await _getFileUri(); switch (selected) { case _Algorithm.zeroDce: - await ImageProcessor.zeroDce(imageUri.toString(), file.filename); + await ImageProcessor.zeroDce( + "${account.url}/${file.path}", + file.filename, + headers: { + "Authorization": Api.getAuthorizationHeaderValue(account), + }, + ); break; } } @@ -104,22 +107,6 @@ class EnhanceHandler { ), ]; - Future _getFileUri() async { - final f = await LargeImageCacheManager.inst.getSingleFile( - api_util.getFilePreviewUrl( - account, - file, - width: k.photoLargeSize, - height: k.photoLargeSize, - a: true, - ), - headers: { - "Authorization": Api.getAuthorizationHeaderValue(account), - }, - ); - return f.absolute.uri; - } - final Account account; final File file; diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt index 651aca66..53826f9b 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt @@ -5,11 +5,9 @@ import android.net.Uri interface MessageEvent data class ImageProcessorCompletedEvent( - val image: Uri, val result: Uri, ) : MessageEvent data class ImageProcessorFailedEvent( - val image: Uri, val exception: Throwable, ) : MessageEvent diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Exception.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Exception.kt index e132c15a..a43eda4a 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Exception.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Exception.kt @@ -1,3 +1,5 @@ package com.nkming.nc_photos.plugin class PermissionException(message: String) : Exception(message) + +class HttpException(statusCode: Int, message: String): Exception(message) diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt index a299d5d8..22348686 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt @@ -20,7 +20,8 @@ class ImageProcessorChannelHandler(context: Context) : "zeroDce" -> { try { zeroDce( - call.argument("image")!!, + call.argument("fileUrl")!!, + call.argument("headers"), call.argument("filename")!!, result ) @@ -42,14 +43,18 @@ class ImageProcessorChannelHandler(context: Context) : } private fun zeroDce( - image: String, filename: String, result: MethodChannel.Result + fileUrl: String, headers: Map?, filename: String, + result: MethodChannel.Result ) { val intent = Intent(context, ImageProcessorService::class.java).apply { putExtra( ImageProcessorService.EXTRA_METHOD, ImageProcessorService.METHOD_ZERO_DCE ) - putExtra(ImageProcessorService.EXTRA_IMAGE, image) + putExtra(ImageProcessorService.EXTRA_FILE_URL, fileUrl) + putExtra( + ImageProcessorService.EXTRA_HEADERS, + headers?.let { HashMap(it) }) putExtra(ImageProcessorService.EXTRA_FILENAME, filename) } ContextCompat.startForegroundService(context, intent) diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt index dfca302d..fc2c9177 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt @@ -18,12 +18,16 @@ import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.nkming.nc_photos.plugin.image_processor.ZeroDce +import java.io.File +import java.net.HttpURLConnection +import java.net.URL class ImageProcessorService : Service() { companion object { const val EXTRA_METHOD = "method" const val METHOD_ZERO_DCE = "zero-dce" - const val EXTRA_IMAGE = "image" + const val EXTRA_FILE_URL = "fileUrl" + const val EXTRA_HEADERS = "headers" const val EXTRA_FILENAME = "filename" private const val NOTIFICATION_ID = @@ -45,6 +49,7 @@ class ImageProcessorService : Service() { super.onCreate() wakeLock.acquire() createNotificationChannel() + cleanUp() } override fun onDestroy() { @@ -55,7 +60,7 @@ class ImageProcessorService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { assert(intent.hasExtra(EXTRA_METHOD)) - assert(intent.hasExtra(EXTRA_IMAGE)) + assert(intent.hasExtra(EXTRA_FILE_URL)) if (!isForeground) { try { startForeground(NOTIFICATION_ID, buildNotification()) @@ -73,19 +78,23 @@ class ImageProcessorService : Service() { Log.e(TAG, "Unknown method: $method") // we can't call stopSelf here as it'll stop the service even if // there are commands running in the bg - addCommand( - ImageProcessorCommand(startId, "null", Uri.EMPTY, "") - ) + addCommand(ImageProcessorCommand(startId, "null", "", null, "")) } } return START_REDELIVER_INTENT } private fun onZeroDce(startId: Int, extras: Bundle) { - val imageUri = Uri.parse(extras.getString(EXTRA_IMAGE)!!) + val fileUrl = extras.getString(EXTRA_FILE_URL)!! + + @Suppress("Unchecked_cast") + val headers = + extras.getSerializable(EXTRA_HEADERS) as HashMap? val filename = extras.getString(EXTRA_FILENAME)!! addCommand( - ImageProcessorCommand(startId, METHOD_ZERO_DCE, imageUri, filename) + ImageProcessorCommand( + startId, METHOD_ZERO_DCE, fileUrl, headers, filename + ) ) } @@ -186,6 +195,17 @@ class ImageProcessorService : Service() { } } + /** + * Clean up temp files in case the service ended prematurely last time + */ + private fun cleanUp() { + try { + getTempDir(this).deleteRecursively() + } catch (e: Throwable) { + Log.e(TAG, "[cleanUp] Failed while cleanUp", e) + } + } + private var isForeground = false private val cmds = mutableListOf() private var cmdTask: ImageProcessorCommandTask? = null @@ -205,7 +225,8 @@ class ImageProcessorService : Service() { private data class ImageProcessorCommand( val startId: Int, val method: String, - val uri: Uri, + val fileUrl: String, + val headers: Map?, val filename: String, val args: Map = mapOf(), ) @@ -222,18 +243,62 @@ private open class ImageProcessorCommandTask(context: Context) : ): MessageEvent { val cmd = params[0]!! return try { + val outUri = handleCommand(cmd) + ImageProcessorCompletedEvent(outUri) + } catch (e: Throwable) { + Log.e(TAG, "[doInBackground] Failed while handleCommand", e) + ImageProcessorFailedEvent(e) + } + } + + private fun handleCommand(cmd: ImageProcessorCommand): Uri { + val file = downloadFile(cmd.fileUrl, cmd.headers) + return try { + val fileUri = Uri.fromFile(file) val output = when (cmd.method) { ImageProcessorService.METHOD_ZERO_DCE -> ZeroDce(context).infer( - cmd.uri + fileUri ) else -> throw IllegalArgumentException( "Unknown method: ${cmd.method}" ) } - val uri = saveBitmap(output, cmd.filename) - ImageProcessorCompletedEvent(cmd.uri, uri) - } catch (e: Throwable) { - ImageProcessorFailedEvent(cmd.uri, e) + saveBitmap(output, cmd.filename) + } finally { + file.delete() + } + } + + private fun downloadFile( + fileUrl: String, headers: Map? + ): File { + Log.i(TAG, "[downloadFile] $fileUrl") + return (URL(fileUrl).openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + instanceFollowRedirects = true + connectTimeout = 8000 + readTimeout = 15000 + for (entry in (headers ?: mapOf()).entries) { + setRequestProperty(entry.key, entry.value) + } + }.use { + val responseCode = it.responseCode + if (responseCode / 100 == 2) { + val file = + File.createTempFile("img", null, getTempDir(context)) + file.outputStream().use { oStream -> + it.inputStream.copyTo(oStream) + } + file + } else { + Log.e( + TAG, + "[downloadFile] Failed downloading file: HTTP$responseCode" + ) + throw HttpException( + responseCode, "Failed downloading file (HTTP$responseCode)" + ) + } } } @@ -248,3 +313,14 @@ private open class ImageProcessorCommandTask(context: Context) : @SuppressLint("StaticFieldLeak") private val context = context } + +private fun getTempDir(context: Context): File { + val f = File(context.cacheDir, "imageProcessor") + if (!f.exists()) { + f.mkdirs() + } else if (!f.isDirectory) { + f.delete() + f.mkdirs() + } + return f +} diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt index 79100b89..fa72fd3e 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt @@ -2,6 +2,7 @@ package com.nkming.nc_photos.plugin import android.app.PendingIntent import android.os.Build +import java.net.HttpURLConnection fun getPendingIntentFlagImmutable(): Int { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) @@ -12,3 +13,12 @@ fun getPendingIntentFlagMutable(): Int { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 } + +inline fun HttpURLConnection.use(block: (HttpURLConnection) -> T): T { + try { + connect() + return block(this) + } finally { + disconnect() + } +} diff --git a/plugin/lib/src/image_processor.dart b/plugin/lib/src/image_processor.dart index f47568b0..d2d69c6f 100644 --- a/plugin/lib/src/image_processor.dart +++ b/plugin/lib/src/image_processor.dart @@ -4,9 +4,14 @@ import 'package:flutter/services.dart'; import 'package:nc_photos_plugin/src/k.dart' as k; class ImageProcessor { - static Future zeroDce(String image, String filename) => + static Future zeroDce( + String fileUrl, + String filename, { + Map? headers, + }) => _methodChannel.invokeMethod("zeroDce", { - "image": image, + "fileUrl": fileUrl, + "headers": headers, "filename": filename, });