{"id":418,"date":"2021-12-03T02:46:36","date_gmt":"2021-12-03T02:46:36","guid":{"rendered":"https:\/\/writingagame.com\/?p=418"},"modified":"2022-01-19T06:39:54","modified_gmt":"2022-01-19T06:39:54","slug":"chapter-9-saving-android-assets-to-files","status":"publish","type":"post","link":"https:\/\/writingagame.com\/index.php\/2021\/12\/03\/chapter-9-saving-android-assets-to-files\/","title":{"rendered":"Chapter 9. Saving Android assets to files"},"content":{"rendered":"\n<p>Though we can access and use Android assets directly (as we just did in previous chapter). we still have to keep in mind, that assets are NOT files in the usual sense. The difference:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Assets can&#8217;t be accessed via common files tools. You have to use special Assets interface.<\/li><li>They are read-only. You can&#8217;t modify them or create new ones programmatically.<\/li><li>They are stored as a part of the APK in compressed form. Assets interface just extracts them on the fly when requested.<\/li><\/ul>\n\n\n\n<p>No doubts that eventually we&#8217;ll have to deal with normal files too. It can be program-generated files, customer&#8217;s data or files downloaded from the Web. Anyway, we won&#8217;t be able to treat them the same way as assets.<\/p>\n\n\n\n<p>Sure, we can treat assets as assets and files as files. However, such dualistic approach doesn&#8217;t look very consistent. Besides, it will require separate implementation or imitation on the Windows side, which seems confusing and unreasonable.<\/p>\n\n\n\n<p>In short words, I think, the best bet is to unpack assets and to save them as usual files, so we can use normal files toolkit consistently across all our platforms.<\/p>\n\n\n\n<p>Again the problem. Assets NDK interface sees files only (with a path as a part of asset name), not sub-folders. So, it doesn&#8217;t leave an option to read what&#8217;s inside of &#8220;dt&#8221; and so on. However, it is possible to read folder&#8217;s content via Java Layer Interface.  I will use  <strong>list_assets()<\/strong> function by  <em>Marcel Smit<\/em>. This function returns given folder&#8217;s content in a form of strings vector. For example, it tells that &#8220;dt&#8221; folder contains 1 item &#8220;shaders&#8221;, and so on. <\/p>\n\n\n\n<p>Using this function we CAN recursively scan assets with all sub-folders starting from &#8220;dt&#8221; and copy files to a file system. <\/p>\n\n\n\n<p>For saving (writing) files we&#8217;ll need to figure out files root directory, like in Windows. Just in case, mine<em> <\/em>is <em>\/data\/user\/0\/com.p_android\/files<\/em><\/p>\n\n\n\n<p>One more thing: we don&#8217;t want to run assets update every time. We want it only once after APK installed or updated. We will save APK&#8217;s timestamp in a separate file and then compare APK&#8217;s timestamp with saved timestamp from the file. If they match, assets are fresh enough, if not &#8211; run the update.  After assets update is finished, we will create or overwrite saved timestamp file.<\/p>\n\n\n\n<p>So, <em>main.cpp<\/em> now is:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [9,11,164,196,218,251,289,345,350]; title: ; notranslate\" title=\"\">\ntypedef long unsigned int size_t;\n\n#include &quot;platform.h&quot;\n#include &quot;TheGame.h&quot;\n\n#include &lt;string&gt;\n#include &lt;vector&gt;\n\n#include &lt;sys\/stat.h&gt;\t\/\/mkdir for Android\n\nstd::string filesRoot;\n\nTheGame theGame;\n\nstruct android_app* androidApp;\n\nASensorManager* sensorManager;\nconst ASensor* accelerometerSensor;\nASensorEventQueue* sensorEventQueue;\n\nEGLDisplay androidDisplay;\nEGLSurface androidSurface;\nEGLContext androidContext;\n\n\/**\n* Initialize an EGL context for the current display.\n*\/\nstatic int engine_init_display(struct engine* engine) {\n\t\/\/ initialize OpenGL ES and EGL\n\n\t\/*\n\t* Here specify the attributes of the desired configuration.\n\t* Below, we select an EGLConfig with at least 8 bits per color\n\t* component compatible with on-screen windows\n\t*\/\n\tconst EGLint attribs&#91;] = {\n\t\tEGL_SURFACE_TYPE, EGL_WINDOW_BIT,\n\t\tEGL_BLUE_SIZE, 8,\n\t\tEGL_GREEN_SIZE, 8,\n\t\tEGL_RED_SIZE, 8,\n\t\tEGL_NONE\n\t};\n\tEGLint format;\n\tEGLint numConfigs;\n\tEGLConfig config;\n\tEGLSurface surface;\n\tEGLContext context;\n\n\tEGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);\n\n\teglInitialize(display, 0, 0);\n\n\t\/* Here, the application chooses the configuration it desires. In this\n\t* sample, we have a very simplified selection process, where we pick\n\t* the first EGLConfig that matches our criteria *\/\n\teglChooseConfig(display, attribs, &amp;config, 1, &amp;numConfigs);\n\n\t\/* EGL_NATIVE_VISUAL_ID is an attribute of the EGLConfig that is\n\t* guaranteed to be accepted by ANativeWindow_setBuffersGeometry().\n\t* As soon as we picked a EGLConfig, we can safely reconfigure the\n\t* ANativeWindow buffers to match, using EGL_NATIVE_VISUAL_ID. *\/\n\teglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &amp;format);\n\n\tANativeWindow_setBuffersGeometry(androidApp-&gt;window, 0, 0, format);\n\n\tsurface = eglCreateWindowSurface(display, config, androidApp-&gt;window, NULL);\n\n\tEGLint contextAttribs&#91;] =\n\t{\n\t\tEGL_CONTEXT_CLIENT_VERSION, 3,\n\t\tEGL_NONE\n\t};\n\tcontext = eglCreateContext(display, config, NULL, contextAttribs);\n\n\n\tif (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {\n\t\tmylog(&quot;ERROR: Unable to eglMakeCurrent&quot;);\n\t\treturn -1;\n\t}\n\n\tandroidDisplay = display;\n\tandroidContext = context;\n\tandroidSurface = surface;\n\n\t\/\/ Initialize GL state.\n\tglEnable(GL_CULL_FACE);\n\tglDisable(GL_DEPTH_TEST);\n\n\treturn 0;\n}\n\n\n\/**\n* Tear down the EGL context currently associated with the display.\n*\/\nstatic void engine_term_display() {\n\n\tif (androidDisplay != EGL_NO_DISPLAY) {\n\t\teglMakeCurrent(androidDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);\n\t\tif (androidContext != EGL_NO_CONTEXT) {\n\t\t\teglDestroyContext(androidDisplay, androidContext);\n\t\t}\n\t\tif (androidSurface != EGL_NO_SURFACE) {\n\t\t\teglDestroySurface(androidDisplay, androidSurface);\n\t\t}\n\t\teglTerminate(androidDisplay);\n\t}\n\tandroidDisplay = EGL_NO_DISPLAY;\n\tandroidContext = EGL_NO_CONTEXT;\n\tandroidSurface = EGL_NO_SURFACE;\n}\n\n\/**\n* Process the next input event.\n*\/\nstatic int32_t engine_handle_input(struct android_app* app, AInputEvent* event) {\n\tif (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {\n\t\t\/\/engine-&gt;state.x = AMotionEvent_getX(event, 0);\n\t\t\/\/engine-&gt;state.y = AMotionEvent_getY(event, 0);\n\t\treturn 1;\n\t}\n\treturn 0;\n}\n\n\/**\n* Process the next main command.\n*\/\nstatic void engine_handle_cmd(struct android_app* app, int32_t cmd) {\n\tstruct engine* engine = (struct engine*)app-&gt;userData;\n\tswitch (cmd) {\n\tcase APP_CMD_INIT_WINDOW:\n\t\t\/\/ The window is being shown, get it ready.\n\t\tif (androidApp-&gt;window != NULL) {\n\t\t\tengine_init_display(engine);\n\t\t\t\/\/engine_draw_frame(engine);\n\t\t}\n\t\tbreak;\n\tcase APP_CMD_TERM_WINDOW:\n\t\t\/\/ The window is being hidden or closed, clean it up.\n\t\tengine_term_display();\n\t\tbreak;\n\tcase APP_CMD_GAINED_FOCUS:\n\t\t\/\/ When our app gains focus, we start monitoring the accelerometer.\n\t\tif (accelerometerSensor != NULL) {\n\t\t\tASensorEventQueue_enableSensor(sensorEventQueue,\n\t\t\t\taccelerometerSensor);\n\t\t\t\/\/ We&#039;d like to get 60 events per second (in microseconds).\n\t\t\tASensorEventQueue_setEventRate(sensorEventQueue,\n\t\t\t\taccelerometerSensor, (1000L \/ 60) * 1000);\n\t\t}\n\t\tbreak;\n\tcase APP_CMD_LOST_FOCUS:\n\t\t\/\/ When our app loses focus, we stop monitoring the accelerometer.\n\t\t\/\/ This is to avoid consuming battery while not being used.\n\t\tif (accelerometerSensor != NULL) {\n\t\t\tASensorEventQueue_disableSensor(sensorEventQueue,\n\t\t\t\taccelerometerSensor);\n\t\t}\n\t\t\/\/ Also stop animating.\n\t\t\/\/engine_draw_frame(engine);\n\t\tbreak;\n\t}\n}\nstatic std::vector&lt;std::string&gt; list_assets(android_app* app, const char* asset_path)\n{ \/\/by Marcel Smit, stolen from https:\/\/github.com\/android\/ndk-samples\/issues\/603\n\tstd::vector&lt;std::string&gt; result;\n\n\tJNIEnv* env = nullptr;\n\tapp-&gt;activity-&gt;vm-&gt;AttachCurrentThread(&amp;env, nullptr);\n\n\tauto context_object = app-&gt;activity-&gt;clazz;\n\tauto getAssets_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(context_object), &quot;getAssets&quot;, &quot;()Landroid\/content\/res\/AssetManager;&quot;);\n\tauto assetManager_object = env-&gt;CallObjectMethod(context_object, getAssets_method);\n\tauto list_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(assetManager_object), &quot;list&quot;, &quot;(Ljava\/lang\/String;)&#91;Ljava\/lang\/String;&quot;);\n\n\tjstring path_object = env-&gt;NewStringUTF(asset_path);\n\tauto files_object = (jobjectArray)env-&gt;CallObjectMethod(assetManager_object, list_method, path_object);\n\tenv-&gt;DeleteLocalRef(path_object);\n\tauto length = env-&gt;GetArrayLength(files_object);\n\n\tfor (int i = 0; i &lt; length; i++)\n\t{\n\t\tjstring jstr = (jstring)env-&gt;GetObjectArrayElement(files_object, i);\n\t\tconst char* filename = env-&gt;GetStringUTFChars(jstr, nullptr);\n\t\tif (filename != nullptr)\n\t\t{\n\t\t\tresult.push_back(filename);\n\t\t\tenv-&gt;ReleaseStringUTFChars(jstr, filename);\n\t\t}\n\t\tenv-&gt;DeleteLocalRef(jstr);\n\t}\n\tapp-&gt;activity-&gt;vm-&gt;DetachCurrentThread();\n\treturn result;\n}\n\nint updateAssets() {\n\t\/\/get APK apkLastUpdateTime timestamp\n\tJNIEnv* env = nullptr;\n\tandroidApp-&gt;activity-&gt;vm-&gt;AttachCurrentThread(&amp;env, nullptr);\n\tjobject context_object = androidApp-&gt;activity-&gt;clazz;\n\tjmethodID getPackageNameMid_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(context_object), &quot;getPackageName&quot;, &quot;()Ljava\/lang\/String;&quot;);\n\tjstring packageName = (jstring)env-&gt;CallObjectMethod(context_object, getPackageNameMid_method);\n\tjmethodID getPackageManager_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(context_object), &quot;getPackageManager&quot;, &quot;()Landroid\/content\/pm\/PackageManager;&quot;);\n\tjobject packageManager_object = env-&gt;CallObjectMethod(context_object, getPackageManager_method);\n\tjmethodID getPackageInfo_method = env-&gt;GetMethodID(env-&gt;GetObjectClass(packageManager_object), &quot;getPackageInfo&quot;, &quot;(Ljava\/lang\/String;I)Landroid\/content\/pm\/PackageInfo;&quot;);\n\tjobject packageInfo_object = env-&gt;CallObjectMethod(packageManager_object, getPackageInfo_method, packageName, 0x0);\n\tjfieldID updateTimeFid = env-&gt;GetFieldID(env-&gt;GetObjectClass(packageInfo_object), &quot;lastUpdateTime&quot;, &quot;J&quot;);\n\tlong int apkLastUpdateTime = env-&gt;GetLongField(packageInfo_object, updateTimeFid);\n\t\/\/ APK updateTime timestamp retrieved\n\t\/\/ compare with saved timestamp\n\tstd::string updateTimeFilePath = filesRoot + &quot;\/dt\/apk_update_time.bin&quot;;\n\tFILE* inFile = fopen(updateTimeFilePath.c_str(), &quot;r&quot;);\n\tif (inFile != NULL)\n\t{\n\t\tlong int savedUpdateTime;\n\t\tfread(&amp;savedUpdateTime, 1, sizeof(savedUpdateTime), inFile);\n\t\tfclose(inFile);\n\t\tif (savedUpdateTime == apkLastUpdateTime) {\n\t\t\tmylog(&quot;Assets are up to date.\\n&quot;);\n\t\t\treturn 0;\n\t\t}\n\t}\n\t\/\/ if here - need to update assets\n\tAAssetManager* am = androidApp-&gt;activity-&gt;assetManager;\n\tint buffSize = 1000000; \/\/guess, should be enough?\n\tchar* buff = new char&#91;buffSize];\n\n\tstd::vector&lt;std::string&gt; dirsToCheck; \/\/list of assets folders to check\n\tdirsToCheck.push_back(&quot;dt&quot;); \/\/root folder\n\twhile (dirsToCheck.size() &gt; 0) {\n\t\t\/\/open last element from directories vector\n\t\tstd::string dirPath = dirsToCheck.back();\n\t\tdirsToCheck.pop_back(); \/\/delete last element\n\t\t\/\/mylog(&quot;Scanning directory &lt;%s&gt;\\n&quot;, dirPath.c_str());\n\t\t\/\/make sure folder exists on local drive\n\t\tstd::string outPath = filesRoot + &quot;\/&quot; + dirPath; \/\/ .c_str();\n\t\tstruct stat info;\n\t\tint statRC = stat(outPath.c_str(), &amp;info);\n\t\tif (statRC == 0)\n\t\t\tmylog(&quot;%s folder exists.\\n&quot;, outPath.c_str());\n\t\telse {\n\t\t\t\/\/ mylog(&quot;Try to create %s\\n&quot;, outPath.c_str());\n\t\t\tint status = mkdir(outPath.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);\n\t\t\tif (status == 0)\n\t\t\t\tmylog(&quot;%s folder added.\\n&quot;, outPath.c_str());\n\t\t\telse {\n\t\t\t\tmylog(&quot;ERROR creating, status=%d, errno: %s.\\n&quot;, status, std::strerror(errno));\n\t\t\t}\n\t\t}\n\t\t\/\/get folder&#039;s content\n\t\tstd::vector&lt;std::string&gt; dirItems = list_assets(androidApp, dirPath.c_str());\n\t\tint itemsN = dirItems.size();\n\t\t\/\/scan directory items\n\t\tfor (int i = 0; i &lt; itemsN; i++) {\n\t\t\tstd::string itemPath = dirPath + &quot;\/&quot; + dirItems.at(i).c_str();\n\t\t\t\/\/mylog(&quot;New item: &lt;%s&gt; - &quot;, itemPath.c_str());\n\t\t\t\/\/try to open it to see if it&#039;s a file\n\t\t\tAAsset* asset = AAssetManager_open(am, itemPath.c_str(), AASSET_MODE_UNKNOWN);\n\t\t\tif (asset != NULL) {\n\t\t\t\tlong size = AAsset_getLength(asset);\n\t\t\t\t\/\/mylog(&quot;It&#039;s a file, size = %d - &quot;, size);\n\t\t\t\tif (size &gt; buffSize) {\n\t\t\t\t\tmylog(&quot;ERROR in main.cpp-&gt;updateAssets(): File %s is too big, skipped.\\n&quot;, itemPath.c_str());\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tAAsset_read(asset, buff, size);\n\t\t\t\t\toutPath = filesRoot + &quot;\/&quot; + itemPath;\n\t\t\t\t\tFILE* outFile = fopen(outPath.c_str(), &quot;w+&quot;);\n\t\t\t\t\tif (outFile != NULL)\n\t\t\t\t\t{\n\t\t\t\t\t\tfwrite(buff, 1, size, outFile);\n\t\t\t\t\t\tfflush(outFile);\n\t\t\t\t\t\tfclose(outFile);\n\t\t\t\t\t\tmylog(&quot;%s saved\\n&quot;, outPath.c_str());\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t\tmylog(&quot;ERROR in main.cpp-&gt;updateAssets(): Can&#039;t create file %s\\n&quot;, itemPath.c_str());\n\t\t\t\t}\n\t\t\t\tAAsset_close(asset);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tdirsToCheck.push_back(itemPath);\n\t\t\t\t\/\/mylog(&quot;It&#039;s a folder, add to folders list to check.\\n&quot;);\n\t\t\t}\n\t\t}\n\t\tdirItems.clear();\n\t}\n\tdelete&#91;] buff;\n\t\/\/ save updateTime\n\tFILE* outFile = fopen(updateTimeFilePath.c_str(), &quot;w+&quot;);\n\tif (outFile != NULL)\n\t{\n\t\tfwrite(&amp;apkLastUpdateTime, 1, sizeof(apkLastUpdateTime), outFile);\n\t\tfflush(outFile);\n\t\tfclose(outFile);\n\t}\n\telse\n\t\tmylog(&quot;ERROR creating %s\\n&quot;, updateTimeFilePath.c_str());\n\treturn 1;\n}\n\/**\n* This is the main entry point of a native application that is using\n* android_native_app_glue.  It runs in its own thread, with its own\n* event loop for receiving input events and doing other things.\n*\/\nvoid android_main(struct android_app* state) {\n\n\t\/\/state-&gt;userData = &amp;engine;\n\tstate-&gt;onAppCmd = engine_handle_cmd;\n\tstate-&gt;onInputEvent = engine_handle_input;\n\tandroidApp = state;\n\n\t\/\/ Prepare to monitor accelerometer\n\tsensorManager = ASensorManager_getInstance();\n\taccelerometerSensor = ASensorManager_getDefaultSensor(sensorManager,\n\t\tASENSOR_TYPE_ACCELEROMETER);\n\tsensorEventQueue = ASensorManager_createEventQueue(sensorManager,\n\t\tstate-&gt;looper, LOOPER_ID_USER, NULL, NULL);\n\n\t\/\/ Read all pending events.\n\tint ident;\n\tint events;\n\tstruct android_poll_source* source;\n\t\/\/wait for display\n\twhile (androidDisplay == NULL) {\n\t\t\/\/ No display yet.\n\t\t\/\/std::this_thread::sleep_for(std::chrono::seconds(1));\n\t\t\/\/mylog(&quot;No display yet\\n&quot;);\n\t\t\/\/wait for event\n\t\twhile ((ident = ALooper_pollAll(0, NULL, &amp;events,\n\t\t\t(void**)&amp;source)) &gt;= 0) {\n\t\t\t\/\/ Process this event.\n\t\t\tif (source != NULL) {\n\t\t\t\tsource-&gt;process(state, source);\n\t\t\t}\n\t\t}\n\t}\n\n\tEGLint w, h;\n\teglQuerySurface(androidDisplay, androidSurface, EGL_WIDTH, &amp;w);\n\teglQuerySurface(androidDisplay, androidSurface, EGL_HEIGHT, &amp;h);\n\ttheGame.onScreenResize(w, h);\n\n\t\/\/retrieving files root\n\tfilesRoot.assign(androidApp-&gt;activity-&gt;internalDataPath);\n\tmylog(&quot;filesRoot = %s\\n&quot;, filesRoot.c_str());\n\n\tupdateAssets();\n\n\ttheGame.run();\n\n\tengine_term_display();\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Replace <em>main.cpp<\/em> code by this one.<\/p>\n\n\n\n<p>Turn on Android, unlock, plug in, allow.<\/p>\n\n\n\n<p>Start Logcat, mylog, clear, build and run.<\/p>\n\n\n\n<p>In Logcat we can see the assets updating process:<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/writingagame.com\/img\/b01\/c09\/01.jpg\"><\/p>\n\n\n\n<p>Now stop debugging, clear Logcat, and start OurProject <strong>from phone<\/strong>.<\/p>\n\n\n\n<p>What do we have in Logcat this time?:<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/writingagame.com\/img\/b01\/c09\/02.jpg\"><\/p>\n\n\n\n<p><strong>Assets are up to date<\/strong>!<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p class=\"mb-2\">Though we can access and use Android assets directly (as we just did in previous chapter). we still have to keep in mind, that assets are NOT files in the usual sense. The difference: Assets can&#8217;t be accessed via common files tools. You have to use special Assets interface. They are read-only. You can&#8217;t modify [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-418","post","type-post","status-publish","format-standard","hentry","category-cross-platform-3d"],"_links":{"self":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/418","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/comments?post=418"}],"version-history":[{"count":19,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/418\/revisions"}],"predecessor-version":[{"id":1330,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/418\/revisions\/1330"}],"wp:attachment":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media?parent=418"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/categories?post=418"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/tags?post=418"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}