android improvements

This commit is contained in:
mertalev 2025-08-13 18:57:14 -04:00
parent 3100702e93
commit f039672a2a
No known key found for this signature in database
GPG key ID: DF6ABC77AAD98C95
14 changed files with 70 additions and 51 deletions

View file

@ -3,6 +3,8 @@ package app.alextran.immich
import android.os.Build
import android.os.ext.SdkExtensions
import androidx.annotation.NonNull
import app.alextran.immich.images.ThumbnailApi
import app.alextran.immich.images.ThumbnailsImpl
import app.alextran.immich.sync.NativeSyncApi
import app.alextran.immich.sync.NativeSyncApiImpl26
import app.alextran.immich.sync.NativeSyncApiImpl30
@ -22,6 +24,7 @@ class MainActivity : FlutterFragmentActivity() {
} else {
NativeSyncApiImpl30(this)
}
NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl)
NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl)
ThumbnailApi.setUp(flutterEngine.dartExecutor.binaryMessenger, ThumbnailsImpl(this))
}
}

View file

@ -9,10 +9,6 @@ import java.nio.ByteBuffer;
// modified to use native allocations
public final class ThumbHash {
private static native long allocateNative(int size);
private static native ByteBuffer wrapAsBuffer(long address, int capacity);
/**
* Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
*
@ -60,9 +56,8 @@ public final class ThumbHash {
int w = Math.round(ratio > 1.0f ? 32.0f : 32.0f * ratio);
int h = Math.round(ratio > 1.0f ? 32.0f / ratio : 32.0f);
int size = w * h * 4;
long pointer = allocateNative(size);
ByteBuffer buffer = wrapAsBuffer(pointer, size);
byte[] rgba = buffer.array();
long pointer = ThumbnailsImpl.allocateNative(size);
ByteBuffer rgba = ThumbnailsImpl.wrapAsBuffer(pointer, size);
int cx_stop = Math.max(lx, hasAlpha ? 5 : 3);
int cy_stop = Math.max(ly, hasAlpha ? 5 : 3);
float[] fx = new float[cx_stop];
@ -106,10 +101,10 @@ public final class ThumbHash {
float b = l - 2.0f / 3.0f * p;
float r = (3.0f * l - b + q) / 2.0f;
float g = r - q;
rgba[i] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, r)));
rgba[i + 1] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, g)));
rgba[i + 2] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, b)));
rgba[i + 3] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, a)));
rgba.put(i, (byte) Math.max(0, Math.round(255.0f * Math.min(1, r))));
rgba.put(i + 1, (byte) Math.max(0, Math.round(255.0f * Math.min(1, g))));
rgba.put(i + 2, (byte) Math.max(0, Math.round(255.0f * Math.min(1, b))));
rgba.put(i + 3, (byte) Math.max(0, Math.round(255.0f * Math.min(1, a))));
}
}
return new Image(w, h, pointer);

View file

@ -59,7 +59,7 @@ private open class ThumbnailsPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface ThumbnailApi {
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, callback: (Result<Map<String, Long>>) -> Unit)
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result<Map<String, Long>>) -> Unit)
fun cancelImageRequest(requestId: Long)
fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit)
@ -81,7 +81,8 @@ interface ThumbnailApi {
val requestIdArg = args[1] as Long
val widthArg = args[2] as Long
val heightArg = args[3] as Long
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg) { result: Result<Map<String, Long>> ->
val isVideoArg = args[4] as Boolean
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result<Map<String, Long>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(ThumbnailsPigeonUtils.wrapError(error))

View file

@ -21,6 +21,7 @@ import com.bumptech.glide.load.DecodeFormat
import java.util.Base64
import java.util.HashMap
import java.util.concurrent.CancellationException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Future
data class Request(
@ -33,9 +34,10 @@ data class Request(
class ThumbnailsImpl(context: Context) : ThumbnailApi {
private val ctx: Context = context.applicationContext
private val resolver: ContentResolver = ctx.contentResolver
private val requestThread = Executors.newSingleThreadExecutor()
private val threadPool =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() / 2 + 1)
private val requestMap = HashMap<Long, Request>()
private val requestMap = ConcurrentHashMap<Long, Request>()
companion object {
val PROJECTION = arrayOf(
@ -75,9 +77,9 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
"height" to image.height.toLong()
)
callback(Result.success(res))
} catch (e: Exception) {
} catch (e: Exception) {
callback(Result.failure(e))
}
}
}
}
@ -86,12 +88,13 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
requestId: Long,
width: Long,
height: Long,
isVideo: Boolean,
callback: (Result<Map<String, Long>>) -> Unit
) {
val signal = CancellationSignal()
val task = threadPool.submit {
try {
getThumbnailBufferInternal(assetId, width, height, callback, signal)
getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal)
} catch (e: Exception) {
when (e) {
is OperationCanceledException -> callback(CANCELLED)
@ -102,7 +105,8 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
requestMap.remove(requestId)
}
}
requestMap[requestId] = Request(requestId, task, signal, callback)
val request = Request(requestId, task, signal, callback)
requestMap[requestId] = request
}
override fun cancelImageRequest(requestId: Long) {
@ -110,7 +114,12 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
request.taskFuture.cancel(false)
request.cancellationSignal.cancel()
if (request.taskFuture.isCancelled) {
request.callback(CANCELLED)
requestThread.execute {
try {
request.callback(CANCELLED)
} catch (_: Exception) {
}
}
}
}
@ -118,6 +127,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
assetId: String,
width: Long,
height: Long,
isVideo: Boolean,
callback: (Result<Map<String, Long>>) -> Unit,
signal: CancellationSignal
) {
@ -126,24 +136,14 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
val targetHeight = height.toInt()
val id = assetId.toLong()
val cursor = resolver.query(URI, PROJECTION, SELECTION, arrayOf(assetId), null)
?: return callback(Result.failure(RuntimeException("Asset not found")))
signal.throwIfCanceled()
cursor.use { c ->
if (!c.moveToNext()) {
return callback(Result.failure(RuntimeException("Asset not found")))
}
val mediaType = c.getInt(1)
val bitmap = when (mediaType) {
MEDIA_TYPE_IMAGE -> decodeImage(id, targetWidth, targetHeight, signal)
MEDIA_TYPE_VIDEO -> decodeVideoThumbnail(id, targetWidth, targetHeight, signal)
else -> return callback(Result.failure(RuntimeException("Unsupported media type")))
}
processBitmap(bitmap, callback, signal)
val bitmap = if (isVideo) {
decodeVideoThumbnail(id, targetWidth, targetHeight, signal)
} else {
decodeImage(id, targetWidth, targetHeight, signal)
}
processBitmap(bitmap, callback, signal)
}
private fun processBitmap(
@ -162,6 +162,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
signal.throwIfCanceled()
val buffer = wrapAsBuffer(pointer, size)
bitmap.copyPixelsToBuffer(buffer)
bitmap.recycle()
signal.throwIfCanceled()
val res = mapOf(
"pointer" to pointer,