changeset 2658:ad6fc7f1a9af

Android: Asset/Resource refactoring for compatibility with other platforms. Other platforms are able to access the bundled assets via the filesystem. Android keeps the assets in the Zipped APK archive, so during initialization we now extract the non-resource (numeric images) into the app cache dir. dw_app_dir() now returns the path to the application cache. dw_user_dir() returns either the HOME directory or the path to the application root directory (usually the parent of the cache).
author bsmith@81767d24-ef19-dc11-ae90-00e081727c95
date Fri, 24 Sep 2021 13:57:16 +0000
parents 9535f533a230
children 3a14d7fd4b99
files android/DWindows.kt android/dw.cpp
diffstat 2 files changed, 89 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/android/DWindows.kt	Sun Sep 19 21:42:00 2021 +0000
+++ b/android/DWindows.kt	Fri Sep 24 13:57:16 2021 +0000
@@ -53,10 +53,14 @@
 import com.google.android.material.tabs.TabLayout
 import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
 import com.google.android.material.tabs.TabLayoutMediator
+import java.io.BufferedInputStream
 import java.io.File
+import java.io.FileOutputStream
 import java.io.IOException
 import java.util.*
 import java.util.concurrent.locks.ReentrantLock
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
 
 object DWEvent {
     const val TIMER = 0
@@ -81,6 +85,8 @@
     const val HTML_CHANGED = 19
 }
 
+val DWImageExts = arrayOf("", ".png", ".webp", ".jpg", ".jpeg", ".gif")
+
 class DWTabViewPagerAdapter : RecyclerView.Adapter<DWTabViewPagerAdapter.DWEventViewHolder>() {
     val viewList = mutableListOf<LinearLayout>()
     val pageList = mutableListOf<Long>()
@@ -796,6 +802,60 @@
           }
     }
 
+    // Returns true if the filename is a resource ID (non-zero number)
+    // with a image file extension in our DWImageExts list
+    private fun isDWResource(filename: String): Boolean {
+        val length = filename.length
+
+        for (ext in DWImageExts) {
+            if (ext.isNotEmpty() && filename.endsWith(ext)) {
+                val filebody: String = filename.substring(7, length - ext.length)
+                if (filebody.toInt() > 0) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    /**
+     * extracts assets/ in the APK to the application cache directory
+     */
+    private fun extractAssets() {
+        var zipFile: ZipFile? = null
+        val targetDir = cacheDir
+
+        try {
+            zipFile = ZipFile(this.applicationInfo.sourceDir)
+            val e: Enumeration<out ZipEntry?> = zipFile.entries()
+            while (e.hasMoreElements()) {
+                val entry: ZipEntry? = e.nextElement()
+                if (entry == null || entry.isDirectory || !entry.name.startsWith("assets/") ||
+                        isDWResource(entry.name)) {
+                    continue
+                }
+                val targetFile = File(targetDir, entry.name.substring("assets/".length))
+                targetFile.parentFile!!.mkdirs()
+                val tempBuffer = ByteArray(entry.size.toInt())
+                var ais: BufferedInputStream? = null
+                var aos: FileOutputStream? = null
+                try {
+                    ais = BufferedInputStream(zipFile.getInputStream(entry))
+                    aos = FileOutputStream(targetFile)
+                    ais.read(tempBuffer)
+                    aos.write(tempBuffer)
+                } catch (e: IOException) {
+                } finally {
+                    ais?.close()
+                    aos?.close()
+                }
+            }
+        } catch (e: IOException) {
+        } finally {
+            zipFile?.close()
+        }
+    }
+
     // We only want to call this once when the app starts up
     // By default Android will call onCreate for rotation and other
     // changes.  This is incompatible with Dynamic Windows...
@@ -812,10 +872,16 @@
         var s = packageName
         val p = m.getPackageInfo(s!!, 0)
         s = p.applicationInfo.dataDir
+        val c = cacheDir.path
+
+        // Extract any non-resource assets to the cache directory
+        // So that our C code can access them as files, like on
+        // other Dynamic Windows platforms
+        extractAssets()
 
         // Initialize the Dynamic Windows code...
         // This will start a new thread that calls the app's dwmain()
-        dwindowsInit(s, this.getPackageName())
+        dwindowsInit(s, c, this.getPackageName())
     }
 
     override fun onConfigurationChanged(newConfig: Configuration) {
@@ -1466,7 +1532,6 @@
         waitOnUiThread {
             button = ImageButton(this)
             val dataArrayMap = SimpleArrayMap<String, Long>()
-            val exts = arrayOf("", ".png", ".webp", ".jpg", ".jpeg", ".gif")
             var filename: String? = null
 
             button!!.tag = dataArrayMap
@@ -1482,7 +1547,7 @@
             }
 
             if(filename != null) {
-                for (ext in exts) {
+                for (ext in DWImageExts) {
                     // Try to load the image, and protect against exceptions
                     try {
                         val f = this.assets.open(filename + ext)
@@ -1505,7 +1570,6 @@
         waitOnUiThread {
             button = ImageButton(this)
             val dataArrayMap = SimpleArrayMap<String, Long>()
-            val exts = arrayOf("", ".png", ".webp", ".jpg", ".jpeg", ".gif")
 
             button!!.tag = dataArrayMap
             button!!.id = cid
@@ -1514,7 +1578,7 @@
                 eventHandlerSimple(button!!, DWEvent.CLICKED)
             }
 
-            for (ext in exts) {
+            for (ext in DWImageExts) {
                 // Try to load the image, and protect against exceptions
                 try {
                     val f = this.assets.open(filename + ext)
@@ -2980,9 +3044,7 @@
                 }
             }
             if(filename != null) {
-                val exts = arrayOf("", ".png", ".webp", ".jpg", ".jpeg", ".gif")
-
-                for (ext in exts) {
+                for (ext in DWImageExts) {
                     // Try to load the image, and protect against exceptions
                     try {
                         val f = this.assets.open(filename + ext)
@@ -3062,9 +3124,7 @@
             // Handle filename or DW resource IDs
             // these will be located in the assets folder
             if(filename != null) {
-                val exts = arrayOf("", ".png", ".webp", ".jpg", ".jpeg", ".gif")
-
-                for (ext in exts) {
+                for (ext in DWImageExts) {
                     // Try to load the image, and protect against exceptions
                     try {
                         val f = this.assets.open(filename + ext)
@@ -3099,9 +3159,7 @@
                 filename = file
             }
             if(filename != null) {
-                val exts = arrayOf("", ".png", ".webp", ".jpg", ".jpeg", ".gif")
-
-                for (ext in exts) {
+                for (ext in DWImageExts) {
                     // Try to load the image, and protect against exceptions
                     try {
                         val f = this.assets.open(filename + ext)
@@ -3984,7 +4042,7 @@
      * Native methods that are implemented by the 'dwindows' native library,
      * which is packaged with this application.
      */
-    external fun dwindowsInit(dataDir: String, appid: String)
+    external fun dwindowsInit(dataDir: String, cacheDir: String, appid: String)
     external fun eventHandler(
         obj1: View?,
         obj2: View?,
--- a/android/dw.cpp	Sun Sep 19 21:42:00 2021 +0000
+++ b/android/dw.cpp	Fri Sep 24 13:57:16 2021 +0000
@@ -48,6 +48,7 @@
 static char _dw_app_id[_DW_APP_ID_SIZE+1]= {0};
 static char _dw_app_name[_DW_APP_ID_SIZE+1]= {0};
 static char _dw_exec_dir[MAX_PATH+1] = {0};
+static char _dw_user_dir[MAX_PATH+1] = {0};
 static int _dw_android_api = 0;
 
 static pthread_key_t _dw_env_key;
@@ -162,10 +163,12 @@
  *      path: The path to the Android app.
  */
 JNIEXPORT void JNICALL
-Java_org_dbsoft_dwindows_DWindows_dwindowsInit(JNIEnv* env, jobject obj, jstring path, jstring appID)
-{
-    char *arg = strdup(env->GetStringUTFChars((jstring)path, nullptr));
+Java_org_dbsoft_dwindows_DWindows_dwindowsInit(JNIEnv* env, jobject obj, jstring apppath, jstring appcache, jstring appID)
+{
+    char *arg = strdup(env->GetStringUTFChars((jstring)apppath, nullptr));
+    const char *cache = env->GetStringUTFChars((jstring)appcache, nullptr);
     const char *appid = env->GetStringUTFChars((jstring)appID, nullptr);
+    char *home = getenv("HOME");
 
     if(!_dw_main_event)
     {
@@ -184,11 +187,18 @@
         _dw_main_event = dw_event_new();
     }
 
-    if(arg)
+    if(cache)
     {
         /* Store the passed in path for dw_app_dir() */
-        strncpy(_dw_exec_dir, arg, MAX_PATH);
-    }
+        strncpy(_dw_exec_dir, cache, MAX_PATH);
+    }
+    /* Store the best path for dw_user_dir() */
+    if(home)
+        strncpy(_dw_user_dir, home, MAX_PATH);
+    else if(arg)
+        strncpy(_dw_user_dir, arg, MAX_PATH);
+    else
+        strcpy(_dw_user_dir, "/");
     if(appid)
     {
         /* Store our reported Android AppID */
@@ -1050,19 +1060,6 @@
  */
 char * API dw_user_dir(void)
 {
-    static char _dw_user_dir[MAX_PATH+1] = {0};
-
-    if(!_dw_user_dir[0])
-    {
-        char *home = getenv("HOME");
-
-        if(home)
-            strcpy(_dw_user_dir, home);
-        else if(_dw_exec_dir[0])
-            strcpy(_dw_user_dir, _dw_exec_dir);
-        else
-            strcpy(_dw_user_dir, "/");
-    }
     return _dw_user_dir;
 }