mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 07:44:34 +03:00
feat: implement media handling via native bridge for Android
This commit is contained in:
@@ -34,6 +34,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
package com.thefeed.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Base64
|
||||
import android.webkit.JavascriptInterface
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
class AndroidBridge(private val activity: Activity) {
|
||||
@@ -54,6 +64,83 @@ class AndroidBridge(private val activity: Activity) {
|
||||
return sha256(password) == stored
|
||||
}
|
||||
|
||||
// ===== Media handoff to system apps =====
|
||||
// The web frontend calls these for save / open / share when it
|
||||
// detects window.Android — WebView can't natively download blob URLs,
|
||||
// navigate to blob URLs in new tabs, or do navigator.share with files.
|
||||
|
||||
@JavascriptInterface
|
||||
fun openMedia(base64: String, mime: String, filename: String): Boolean {
|
||||
return try {
|
||||
val uri = writeToCache(base64, filename)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, sanitiseMime(mime))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
activity.startActivity(Intent.createChooser(intent, filename))
|
||||
true
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun shareMedia(base64: String, mime: String, filename: String): Boolean {
|
||||
return try {
|
||||
val uri = writeToCache(base64, filename)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = sanitiseMime(mime)
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
activity.startActivity(Intent.createChooser(intent, filename))
|
||||
true
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun saveMedia(base64: String, mime: String, filename: String): Boolean {
|
||||
return try {
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val safe = sanitiseFilename(filename)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val resolver = activity.contentResolver
|
||||
val collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, safe)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, sanitiseMime(mime))
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
val target = resolver.insert(collection, values) ?: return false
|
||||
resolver.openOutputStream(target)?.use { it.write(bytes) }
|
||||
true
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val out = File(dir, safe)
|
||||
FileOutputStream(out).use { it.write(bytes) }
|
||||
true
|
||||
}
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
private fun writeToCache(base64: String, filename: String): Uri {
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val dir = File(activity.cacheDir, "shared")
|
||||
dir.mkdirs()
|
||||
val out = File(dir, sanitiseFilename(filename))
|
||||
FileOutputStream(out).use { it.write(bytes) }
|
||||
return FileProvider.getUriForFile(activity, activity.packageName + ".fileprovider", out)
|
||||
}
|
||||
|
||||
private fun sanitiseFilename(name: String): String {
|
||||
val cleaned = name.replace(Regex("[^A-Za-z0-9._-]"), "_").trim('_', '.')
|
||||
return if (cleaned.isEmpty()) "media" else cleaned
|
||||
}
|
||||
|
||||
private fun sanitiseMime(m: String): String {
|
||||
return if (m.matches(Regex("^[\\w./+-]+$"))) m else "application/octet-stream"
|
||||
}
|
||||
|
||||
private fun sha256(input: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(input.toByteArray(Charsets.UTF_8))
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="shared" path="shared/" />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user