@@ -10,11 +10,17 @@ import android.database.Cursor
1010import android.graphics.Bitmap
1111import android.graphics.Point
1212import android.net.Uri
13+ import android.os.Build
1314import android.os.Bundle
1415import android.os.CancellationSignal
16+ import android.os.Handler
17+ import android.os.HandlerThread
1518import android.os.OperationCanceledException
1619import android.os.ParcelFileDescriptor
20+ import android.os.ProxyFileDescriptorCallback
21+ import android.os.storage.StorageManager
1722import android.util.Log
23+ import androidx.core.content.getSystemService
1824import androidx.core.os.BundleCompat
1925import coil3.ColorImage
2026import coil3.decode.ContentMetadata
@@ -28,19 +34,27 @@ import coil3.toBitmap
2834import kotlinx.coroutines.CancellationException
2935import kotlinx.coroutines.CompletableDeferred
3036import kotlinx.coroutines.CoroutineScope
37+ import kotlinx.coroutines.CoroutineStart
3138import kotlinx.coroutines.Dispatchers
32- import kotlinx.coroutines.async
33- import kotlinx.coroutines.coroutineScope
39+ import kotlinx.coroutines.InternalCoroutinesApi
40+ import kotlinx.coroutines.Job
41+ import kotlinx.coroutines.SupervisorJob
42+ import kotlinx.coroutines.TimeoutCancellationException
43+ import kotlinx.coroutines.cancel
3444import kotlinx.coroutines.currentCoroutineContext
45+ import kotlinx.coroutines.ensureActive
3546import kotlinx.coroutines.job
3647import kotlinx.coroutines.launch
3748import kotlinx.coroutines.runBlocking
49+ import kotlinx.coroutines.withTimeout
3850import okio.buffer
3951import okio.sink
4052import org.akanework.gramophone.BuildConfig
4153import org.akanework.gramophone.logic.utils.CoilArtPipeline
42- import java.io.FileOutputStream
54+ import java.io.ByteArrayOutputStream
4355import java.io.IOException
56+ import java.io.OutputStream
57+ import kotlin.time.Duration.Companion.milliseconds
4458
4559/* *
4660 * ContentProvider that serves album artwork to external processes (e.g. Android Auto).
@@ -87,11 +101,22 @@ class GramophoneAlbumArtProvider : ContentProvider() {
87101
88102 override fun onCreate () = true
89103
90- private suspend fun CoroutineScope. openFileCommon ( uri : Uri , size : Point ? ,
91- allowPartial : Boolean ): AssetFileDescriptor ? {
104+ @OptIn( InternalCoroutinesApi :: class )
105+ private suspend fun openFileCommon ( uri : Uri , size : Point ? , allowPartial : Boolean ): AssetFileDescriptor ? {
92106 val context = context!!
93107 val cfd = CompletableDeferred <AssetFileDescriptor ?>()
94- launch(Dispatchers .IO ) {
108+ val scope = CoroutineScope (Dispatchers .IO + SupervisorJob ())
109+ currentCoroutineContext().job.invokeOnCompletion {
110+ if (it is CancellationException ) scope.cancel(it)
111+ else if (it != null ) scope.cancel(" Error in parent block" , it)
112+ }
113+ scope.launch {
114+ currentCoroutineContext().job.invokeOnCompletion {
115+ if (! cfd.isCompleted) {
116+ cfd.completeExceptionally(it
117+ ? : IllegalStateException (" Completed job without setting file descriptor" ))
118+ }
119+ }
95120 context.imageLoader.execute(
96121 ImageRequest .Builder (context)
97122 .data(uri)
@@ -110,15 +135,15 @@ class GramophoneAlbumArtProvider : ContentProvider() {
110135 // we can write it there for benefit of UI code somewhere else.
111136 .memoryCachePolicy(CachePolicy .WRITE_ONLY )
112137 .allowHardware(false )
113- .decoderFactory { result, _ , _ ->
138+ .decoderFactory { result, options , _ ->
114139 val src = result.source.source()
115140 src.peek().let { peekSrc ->
116141 if (peekSrc.readByte() != 0xff .toByte() ||
117142 peekSrc.readByte() != 0xd8 .toByte() ||
118143 peekSrc.readByte() != 0xff .toByte() ||
119144 run {
120- val peek = peekSrc.readByte().toInt()
121- peek != 0xdb && peek !in 0xe0 .. 0xef
145+ val peek = peekSrc.readByte()
146+ peek != 0xdb .toByte() && peek !in 0xe0 .toByte() .. 0xef .toByte()
122147 }
123148 ) {
124149 // Not JPEG. We'll have to re-encode to JPEG (here done in target)
@@ -140,18 +165,11 @@ class GramophoneAlbumArtProvider : ContentProvider() {
140165 .parcelFileDescriptor.dup(), it.startOffset, it.declaredLength))
141166 }
142167 } else {
143- val pipe = ParcelFileDescriptor .createPipe()
144- cfd.complete(
145- AssetFileDescriptor (
146- pipe[0 ], 0 ,
147- AssetFileDescriptor .UNKNOWN_LENGTH
148- )
149- )
150- try {
151- FileOutputStream (pipe[1 ].fileDescriptor).sink()
152- .buffer().writeAll(src)
153- } finally {
154- pipe[1 ].close()
168+ writeDataCommon(cfd, scope, options.context) {
169+ if (it != null ) {
170+ it.sink().buffer().writeAll(src); null
171+ } else
172+ src.readByteArray()
155173 }
156174 }
157175 // shareable is false to avoid writing dummy to memory cache
@@ -161,18 +179,18 @@ class GramophoneAlbumArtProvider : ContentProvider() {
161179 )
162180 }
163181 }
164- .target(onSuccess = {
182+ .target(onSuccess = { image ->
165183 if (! cfd.isCompleted) {
166- val pipe = ParcelFileDescriptor .createPipe()
167- cfd.complete(AssetFileDescriptor (
168- pipe[0 ], 0 , AssetFileDescriptor .UNKNOWN_LENGTH ))
169- try {
170- FileOutputStream (pipe[1 ].fileDescriptor).use { os ->
171- it.toBitmap().compress(
172- Bitmap .CompressFormat .JPEG , 95 , os)
184+ launch(start = CoroutineStart .ATOMIC ) {
185+ writeDataCommon(cfd, scope, context) {
186+ val os = it ? : ByteArrayOutputStream ()
187+ image.toBitmap().compress(Bitmap .CompressFormat .JPEG ,
188+ 95 , os)
189+ if (it != null )
190+ null
191+ else
192+ (os as ByteArrayOutputStream ).toByteArray()
173193 }
174- } finally {
175- pipe[1 ].close()
176194 }
177195 }
178196 })
@@ -196,6 +214,90 @@ class GramophoneAlbumArtProvider : ContentProvider() {
196214 return cfd.await()
197215 }
198216
217+ @OptIn(InternalCoroutinesApi ::class )
218+ private suspend fun writeDataCommon (cfd : CompletableDeferred <AssetFileDescriptor ?>,
219+ scope : CoroutineScope , context : Context ,
220+ callback : (OutputStream ? ) -> ByteArray? ) {
221+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
222+ currentCoroutineContext().job.ensureActive()
223+ val bytes = callback(null )!!
224+ currentCoroutineContext().job.ensureActive()
225+ val ht = HandlerThread (" pfd_${System .currentTimeMillis()} " )
226+ ht.start()
227+ // Specifically ImageDecoder on Android P or later needs a seekable file descriptor
228+ val pfd = context.getSystemService<StorageManager >()!! .openProxyFileDescriptor(
229+ ParcelFileDescriptor .MODE_READ_ONLY ,
230+ object : ProxyFileDescriptorCallback () {
231+ override fun onGetSize (): Long {
232+ return bytes.size.toLong()
233+ }
234+
235+ override fun onRead (offset : Long , size : Int , data : ByteArray ): Int {
236+ val offset = offset.toInt()
237+ var size = size
238+ if (offset + size > bytes.size) {
239+ size = bytes.size - offset
240+ }
241+ System .arraycopy(bytes, offset, data, 0 ,
242+ size)
243+ return size
244+ }
245+
246+ override fun onRelease () {
247+ ht.quitSafely()
248+ }
249+ }, Handler (ht.looper)
250+ )
251+ cfd.complete(
252+ AssetFileDescriptor (
253+ pfd, 0 , AssetFileDescriptor .UNKNOWN_LENGTH
254+ )
255+ )
256+ return
257+ }
258+ val pipe = ParcelFileDescriptor .createPipe()
259+ cfd.complete(
260+ AssetFileDescriptor (
261+ pipe[0 ], 0 , AssetFileDescriptor .UNKNOWN_LENGTH
262+ )
263+ )
264+ val disposable = scope.coroutineContext[Job ]!! .invokeOnCompletion(
265+ onCancelling = true ) {
266+ if (it is CancellationException ) {
267+ // this will interrupt write to ensure we don't block forever
268+ pipe[1 ].close()
269+ }
270+ }
271+ try {
272+ withTimeout((30 * 1000 ).milliseconds) {
273+ launch(Dispatchers .IO , start = CoroutineStart .ATOMIC ) {
274+ ParcelFileDescriptor .AutoCloseOutputStream (pipe[1 ])
275+ .use { os ->
276+ // only check for cancel here to ensure close
277+ ensureActive()
278+ try {
279+ callback(os)
280+ } catch (e: Exception ) {
281+ try {
282+ ensureActive()
283+ } catch (e2: CancellationException ) {
284+ Log .w(TAG , " eating exception due" +
285+ " to cancel()" , e)
286+ e2.addSuppressed(e)
287+ throw e2
288+ }
289+ throw e
290+ }
291+ }
292+ disposable.dispose()
293+ }.join()
294+ }
295+ } catch (_: TimeoutCancellationException ) {
296+ // If the other side ain't done reading after 30s, give up
297+ scope.cancel()
298+ }
299+ }
300+
199301 private fun openFileCommon (uri : Uri , size : Point ? , signal : CancellationSignal ? ,
200302 allowPartial : Boolean ): AssetFileDescriptor ? {
201303 if (uri.pathSegments.size < 2 )
0 commit comments