fix(tv-picker): native Android TV image picker for branded/slideshow + bump 1.0.10+11

This commit is contained in:
dwindown
2026-04-05 16:01:46 +07:00
parent 98b8437e87
commit ba407b1848
5 changed files with 404 additions and 35 deletions

View File

@@ -1,5 +1,217 @@
package com.jamshalat.jamshalat_masjid_screen
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileOutputStream
import kotlin.random.Random
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private companion object {
const val CHANNEL_NAME = "jamshalat/tv_media_picker"
const val METHOD_PICK_IMAGES = "pickImages"
const val METHOD_LIST_HANDLERS = "listPickers"
const val REQUEST_PICK_IMAGES = 49011
}
private var pendingResult: MethodChannel.Result? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME)
.setMethodCallHandler { call, result ->
when (call.method) {
METHOD_PICK_IMAGES -> handlePickImages(call, result)
METHOD_LIST_HANDLERS -> result.success(listPickers())
else -> result.notImplemented()
}
}
}
private fun handlePickImages(call: MethodCall, result: MethodChannel.Result) {
if (pendingResult != null) {
result.error("BUSY", "Media picker request is already running.", null)
return
}
val allowMultiple = call.argument<Boolean>("allowMultiple") ?: false
val intent = createPickIntent(allowMultiple)
if (intent == null) {
result.error(
"NO_PICKER",
"No compatible file manager/document picker found.",
listPickers(),
)
return
}
pendingResult = result
startActivityForResult(intent, REQUEST_PICK_IMAGES)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != REQUEST_PICK_IMAGES) return
val result = pendingResult ?: return
pendingResult = null
if (resultCode != Activity.RESULT_OK) {
result.success(emptyList<String>())
return
}
try {
val uris = mutableListOf<Uri>()
data?.data?.let { uris.add(it) }
data?.clipData?.let { clip ->
for (index in 0 until clip.itemCount) {
clip.getItemAt(index).uri?.let { uris.add(it) }
}
}
val uniqueUris = uris.distinctBy { it.toString() }
if (uniqueUris.isEmpty()) {
result.success(emptyList<String>())
return
}
val copiedPaths = mutableListOf<String>()
for (uri in uniqueUris) {
copyUriToCache(uri)?.let { copiedPaths.add(it) }
}
result.success(copiedPaths)
} catch (error: Exception) {
result.error("PICK_FAILED", error.message, null)
}
}
private fun createPickIntent(allowMultiple: Boolean): Intent? {
val openDocumentIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
putExtra(Intent.EXTRA_LOCAL_ONLY, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
)
}
}
if (openDocumentIntent.resolveActivity(packageManager) != null) {
return openDocumentIntent
}
val getContentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
}
if (getContentIntent.resolveActivity(packageManager) != null) {
return Intent.createChooser(getContentIntent, "Pilih gambar")
}
return null
}
private fun listPickers(): List<Map<String, String>> {
val result = LinkedHashMap<String, Map<String, String>>()
val intents = listOf(
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
},
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
},
)
for (intent in intents) {
val resolved = packageManager.queryIntentActivities(intent, 0)
for (info in resolved) {
val packageName = info.activityInfo.packageName ?: continue
if (result.containsKey(packageName)) continue
val label = info.loadLabel(packageManager)?.toString()?.trim().orEmpty()
result[packageName] = mapOf(
"packageName" to packageName,
"label" to if (label.isEmpty()) packageName else label,
)
}
}
return result.values.toList()
}
private fun copyUriToCache(uri: Uri): String? {
if (uri.scheme == "content") {
try {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION,
)
} catch (_: SecurityException) {
// Non-persistable provider, ignore.
} catch (_: UnsupportedOperationException) {
// Provider doesn't support persisted grants.
}
}
val inputStream = contentResolver.openInputStream(uri) ?: return null
inputStream.use { input ->
val ext = resolveExtension(uri)
val folder = File(cacheDir, "picked_images")
if (!folder.exists()) {
folder.mkdirs()
}
val fileName = "img_${System.currentTimeMillis()}_${Random.nextInt(1000, 9999)}.$ext"
val outputFile = File(folder, fileName)
FileOutputStream(outputFile).use { output ->
input.copyTo(output)
}
return outputFile.absolutePath
}
}
private fun resolveExtension(uri: Uri): String {
val mimeType = contentResolver.getType(uri)
val extFromMime = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
if (!extFromMime.isNullOrBlank()) {
return extFromMime
}
val displayName = queryDisplayName(uri)
if (!displayName.isNullOrBlank()) {
val dot = displayName.lastIndexOf('.')
if (dot in 1 until displayName.length - 1) {
return displayName.substring(dot + 1)
}
}
return "jpg"
}
private fun queryDisplayName(uri: Uri): String? {
contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null,
)?.use { cursor ->
if (cursor.moveToFirst()) {
return cursor.getString(0)
}
}
return null
}
}