瀏覽代碼

feat(android): add Plugin assetUrl function (#6299)

Lucas Fernandes Nogueira 2 年之前
父節點
當前提交
dffd8eb5a8

+ 197 - 0
core/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt

@@ -0,0 +1,197 @@
+package app.tauri
+
+import android.content.ContentUris
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Environment
+import android.provider.DocumentsContract
+import android.provider.MediaStore
+import android.provider.OpenableColumns
+import java.io.File
+import java.io.FileOutputStream
+import kotlin.math.min
+
+internal class FsUtils {
+  companion object {
+    fun getFileUrlForUri(context: Context, uri: Uri): String? {
+      // DocumentProvider
+      if (DocumentsContract.isDocumentUri(context, uri)) {
+        // ExternalStorageProvider
+        if (isExternalStorageDocument(uri)) {
+          val docId: String = DocumentsContract.getDocumentId(uri)
+          val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
+            .toTypedArray()
+          val type = split[0]
+          if ("primary".equals(type, ignoreCase = true)) {
+            return legacyPrimaryPath(split[1])
+          } else {
+            val splitIndex = docId.indexOf(':', 1)
+            val tag = docId.substring(0, splitIndex)
+            val path = docId.substring(splitIndex + 1)
+            val nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag)
+            if (nonPrimaryVolume != null) {
+              val result = "$nonPrimaryVolume/$path"
+              val file = File(result)
+              return if (file.exists() && file.canRead()) {
+                result
+              } else null
+            }
+          }
+        } else if (isDownloadsDocument(uri)) {
+          val id: String = DocumentsContract.getDocumentId(uri)
+          val contentUri: Uri = ContentUris.withAppendedId(
+            Uri.parse("content://downloads/public_downloads"),
+            java.lang.Long.valueOf(id)
+          )
+          return getDataColumn(context, contentUri, null, null)
+        } else if (isMediaDocument(uri)) {
+          val docId: String = DocumentsContract.getDocumentId(uri)
+          val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
+            .toTypedArray()
+          val type = split[0]
+          var contentUri: Uri? = null
+          when (type) {
+            "image" -> {
+              contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+            }
+            "video" -> {
+              contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+            }
+            "audio" -> {
+              contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+            }
+          }
+          val selection = "_id=?"
+          val selectionArgs = arrayOf(split[1])
+          if (contentUri != null) {
+            return getDataColumn(context, contentUri, selection, selectionArgs)
+          }
+        }
+      } else if ("content".equals(uri.scheme, ignoreCase = true)) {
+        // Return the remote address
+        return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
+          context,
+          uri,
+          null,
+          null
+        )
+      } else if ("file".equals(uri.scheme, ignoreCase = true)) {
+        return uri.path
+      }
+      return null
+    }
+
+    /**
+     * Get the value of the data column for this Uri. This is useful for
+     * MediaStore Uris, and other file-based ContentProviders.
+     *
+     * @param context The context.
+     * @param uri The Uri to query.
+     * @param selection (Optional) Filter used in the query.
+     * @param selectionArgs (Optional) Selection arguments used in the query.
+     * @return The value of the _data column, which is typically a file path.
+     */
+    private fun getDataColumn(
+      context: Context,
+      uri: Uri,
+      selection: String?,
+      selectionArgs: Array<String>?
+    ): String? {
+      var path: String? = null
+      var cursor: Cursor? = null
+      val column = "_data"
+      val projection = arrayOf(column)
+      try {
+        cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
+        if (cursor != null && cursor.moveToFirst()) {
+          val index = cursor.getColumnIndexOrThrow(column)
+          path = cursor.getString(index)
+        }
+      } catch (ex: IllegalArgumentException) {
+        return getCopyFilePath(uri, context)
+      } finally {
+        cursor?.close()
+      }
+      return path ?: getCopyFilePath(uri, context)
+    }
+
+    private fun getCopyFilePath(uri: Uri, context: Context): String? {
+      val cursor = context.contentResolver.query(uri, null, null, null, null)!!
+      val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+      cursor.moveToFirst()
+      val name = cursor.getString(nameIndex)
+      val file = File(context.filesDir, name)
+      try {
+        val inputStream = context.contentResolver.openInputStream(uri)
+        val outputStream = FileOutputStream(file)
+        var read: Int
+        val maxBufferSize = 1024 * 1024
+        val bufferSize = min(inputStream!!.available(), maxBufferSize)
+        val buffers = ByteArray(bufferSize)
+        while (inputStream.read(buffers).also { read = it } != -1) {
+          outputStream.write(buffers, 0, read)
+        }
+        inputStream.close()
+        outputStream.close()
+      } catch (e: Exception) {
+        return null
+      } finally {
+        cursor.close()
+      }
+      return file.path
+    }
+
+    private fun legacyPrimaryPath(pathPart: String): String {
+      return Environment.getExternalStorageDirectory().toString() + "/" + pathPart
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is ExternalStorageProvider.
+     */
+    private fun isExternalStorageDocument(uri: Uri): Boolean {
+      return "com.android.externalstorage.documents" == uri.authority
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is DownloadsProvider.
+     */
+    private fun isDownloadsDocument(uri: Uri): Boolean {
+      return "com.android.providers.downloads.documents" == uri.authority
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is MediaProvider.
+     */
+    private fun isMediaDocument(uri: Uri): Boolean {
+      return "com.android.providers.media.documents" == uri.authority
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is Google Photos.
+     */
+    private fun isGooglePhotosUri(uri: Uri): Boolean {
+      return "com.google.android.apps.photos.content" == uri.authority
+    }
+
+    private fun getPathToNonPrimaryVolume(context: Context, tag: String): String? {
+      val volumes = context.externalCacheDirs
+      if (volumes != null) {
+        for (volume in volumes) {
+          if (volume != null) {
+            val path = volume.absolutePath
+            val index = path.indexOf(tag)
+            if (index != -1) {
+              return path.substring(0, index) + tag
+            }
+          }
+        }
+      }
+      return null
+    }
+  }
+}

+ 13 - 0
core/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt

@@ -3,8 +3,10 @@ package app.tauri.plugin
 import android.app.Activity
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.net.Uri
 import android.webkit.WebView
 import androidx.core.app.ActivityCompat
+import app.tauri.FsUtils
 import app.tauri.Logger
 import app.tauri.PermissionHelper
 import app.tauri.PermissionState
@@ -50,6 +52,17 @@ abstract class Plugin(private val activity: Activity) {
     return Logger.tags(this.javaClass.simpleName)
   }
 
+  /**
+   * Convert an URI to an URL that can be loaded by the webview.
+   */
+  fun assetUrl(u: Uri): String {
+    var path = FsUtils.getFileUrlForUri(activity, u)
+    if (path?.startsWith("file://") == true) {
+      path = path.replace("file://", "")
+    }
+    return "asset://localhost$path"
+  }
+
   /**
    * Exported plugin method for checking the granted status for each permission
    * declared on the plugin. This plugin call responds with a mapping of permissions to