Lagrange [work/v1.15]

Android: User data safety

=> 4f55dd4a701ff18885c34b4d0939ea2bdd602dce

diff --git a/res/about/android-version.gmi b/res/about/android-version.gmi
index 642dd64a..a5fa91d4 100644
--- a/res/about/android-version.gmi
+++ b/res/about/android-version.gmi
@@ -6,6 +6,11 @@
 ```
 # Release notes
 
+## 1.14 (Beta 11)
+* Try to keep user data safe in unusual situations. Bookmarks and identities are saved via a system API, while also ensuring that data written to storage is not truncated. NOTE: Uninstalling the app will still delete everything, because user data is only stored locally.
+* User data files are now stored in external storage, if available.
+* Fixed determining actual name of files opened via the system file picker. This was sometimes preventing importing user data ZIP archives, for instance.
+
 ## 1.14 (Beta 10)
 * Fixed tab with pinned identity not closing if it's the last tab that's being closed.
 * Fixed hang when setting a folder's parent to itself in the Edit Folder dialog.
diff --git a/src/android.c b/src/android.c
index 7c00df8b..d753d4bb 100644
--- a/src/android.c
+++ b/src/android.c
@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #include "android.h"
 #include "app.h"
+#include "export.h"
 #include "resources.h"
 #include "ui/command.h"
 #include "ui/metrics.h"
@@ -29,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "ui/window.h"
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -225,6 +227,27 @@ int preferredHeight_SystemTextInput(const iSystemTextInput *d) {
     return d->numLines * lineHeight_Text(d->font);
 }
 
+/*----------------------------------------------------------------------------------------------*/
+
+static int userBackupTimer_;
+
+static uint32_t backupUserData_Android_(uint32_t interval, void *data) {
+    userBackupTimer_ = 0;
+    iUnused(interval, data);
+    /* This runs in a background thread. We don't want to block the UI thread for saving. */
+    iExport *backup = new_Export();
+    generatePartial_Export(backup, bookmarks_ExportFlag | identitiesAndTrust_ExportFlag);
+    iBuffer *buf = new_Buffer();
+    openEmpty_Buffer(buf);
+    serialize_Archive(archive_Export(backup), stream_Buffer(buf));
+    delete_Export(backup);
+    iString *enc = base64Encode_Block(data_Buffer(buf));
+    iRelease(buf);
+    javaCommand_Android("backup.save data:%s", cstr_String(enc));
+    delete_String(enc);
+    return 0;
+}
+
 iBool handleCommand_Android(const char *cmd) {
     if (equal_Command(cmd, "android.input.changed")) {
         const int id = argLabel_Command(cmd, "id");
@@ -283,5 +306,29 @@ iBool handleCommand_Android(const char *cmd) {
         }
         return iTrue;
     }
+    else if (equal_Command(cmd, "bookmarks.changed") ||
+             equal_Command(cmd, "idents.changed") ||
+             equal_Command(cmd, "backup.now")) {
+        SDL_RemoveTimer(userBackupTimer_);
+        userBackupTimer_ = SDL_AddTimer(1000, backupUserData_Android_, NULL);
+        return iFalse;
+    }
+    else if (equal_Command(cmd, "backup.found")) {
+        iString *data = suffix_Command(cmd, "data");
+        iBlock *decoded = base64Decode_Block(utf8_String(data));
+        delete_String(data);
+        iArchive *archive = new_Archive();
+        if (openData_Archive(archive, decoded)) {
+            iExport *backup = new_Export();
+            if (load_Export(backup, archive)) {
+                import_Export(backup, ifMissing_ImportMethod, all_ImportMethod,
+                              none_ImportMethod, none_ImportMethod, none_ImportMethod);
+            }
+            delete_Export(backup);
+        }
+        iRelease(archive);
+        delete_Block(decoded);
+        return iTrue;
+    }
     return iFalse;
 }
diff --git a/src/app.c b/src/app.c
index 331f84d9..de7e8f66 100644
--- a/src/app.c
+++ b/src/app.c
@@ -371,10 +371,10 @@ static const char *dataDir_App_(void) {
         d->didCheckDataPathOption = iTrue;
         const iCommandLineArg *arg;
         if ((arg = iClob(checkArgumentValues_CommandLine(
-                 &d->args, userDataDir_CommandLineOption, 1))) != NULL) {
+                &d->args, userDataDir_CommandLineOption, 1))) != NULL) {
             iString *execDir = concatCStr_Path(d->execPath, "..");
             d->overrideDataPath = concat_Path(execDir, value_CommandLineArg(arg, 0));
-            delete_String(execDir);            
+            delete_String(execDir);
         }
     }
     if (d->overrideDataPath) {
@@ -392,6 +392,11 @@ static const char *dataDir_App_(void) {
     if (fileExistsCStr_FileInfo(userDir)) {
         return userDir;
     }
+#endif
+#if defined (iPlatformAndroid)
+    if (SDL_AndroidGetExternalStorageState() & SDL_ANDROID_EXTERNAL_STORAGE_WRITE) {
+        return SDL_AndroidGetExternalStoragePath();
+    }
 #endif
     if (defaultDataDir_App_) {
         return defaultDataDir_App_;
@@ -399,6 +404,59 @@ static const char *dataDir_App_(void) {
     return SDL_GetPrefPath("Jaakko Keränen", "fi.skyjake.lagrange");
 }
 
+#if defined (iPlatformAndroid)
+static void copyFile_(const char *srcPath, const char *dstPath) {
+    if (fileExistsCStr_FileInfo(srcPath)) {
+        iFile *src = newCStr_File(srcPath);
+        iFile *dst = newCStr_File(dstPath);
+        if (open_File(src, readOnly_FileMode) && open_File(dst, writeOnly_FileMode)) {
+            iBlock *data = readAll_File(src);
+            write_File(dst, data);
+            delete_Block(data);
+        }
+        iRelease(dst);
+        iRelease(src);
+    }
+}
+
+static void migrateInternalUserDirToExternalStorage_App_(iApp *d) {
+    if (!(SDL_AndroidGetExternalStorageState() & SDL_ANDROID_EXTERNAL_STORAGE_WRITE)) {
+        /* As a fallback, user data will be stored in internal storage instead. */
+        return;
+    }
+    /* This is the app-specific "files" directory in internal storage. */
+    const char *intDataDir = SDL_GetPrefPath("Jaakko Keränen", "fi.skyjake.lagrange");
+    const char *extDataDir = SDL_AndroidGetExternalStoragePath();
+    const char *names[] = {
+        "bookmarks.ini",
+        "prefs.cfg",
+        "state.lgr",
+        "idents.lgr",
+        "trusted.2.txt",
+        "visited.2.txt",
+    };
+    makeDirs_Path(collectNewCStr_String(extDataDir));
+    iForIndices(i, names) {
+        const char *src = concatPath_CStr(intDataDir, names[i]);
+        copyFile_(src, concatPath_CStr(extDataDir, names[i]));
+        remove(src);
+    }
+    /* Copy identities as well. */
+    const char *srcIdents = concatPath_CStr(intDataDir, "idents");
+    if (fileExistsCStr_FileInfo(srcIdents)) {
+        const char *dstIdents = concatPath_CStr(extDataDir, "idents");
+        makeDirs_Path(collectNewCStr_String(dstIdents));
+        iForEach(DirFileInfo, entry, iClob(newCStr_DirFileInfo(srcIdents))) {
+            const iRangecc name = baseName_Path(path_FileInfo(entry.value));
+            const char *src = cstr_String(path_FileInfo(entry.value));
+            copyFile_(src, concatPath_CStr(dstIdents, cstr_Rangecc(name)));
+            remove(src);
+        }
+        rmdir_Path(collectNewCStr_String(srcIdents));
+    }
+}
+#endif
+
 static const char *downloadDir_App_(void) {
 #if defined (iPlatformAndroidMobile)
     const char *dir = concatPath_CStr(SDL_AndroidGetExternalStoragePath(), "Downloads");
@@ -997,6 +1055,10 @@ static void dumpRequestFinished_App_(void *obj, iGmRequest *req) {
 
 static void init_App_(iApp *d, int argc, char **argv) {
     iBool doDump = iFalse;
+#if defined (iPlatformAndroid)
+    /* Internal storage may be limited in size. */
+    migrateInternalUserDirToExternalStorage_App_(d);
+#endif
 #if defined (iPlatformLinux) && !defined (iPlatformAndroid) && !defined (iPlatformTerminal)
     d->isRunningUnderWindowSystem = !iCmpStr(SDL_GetCurrentVideoDriver(), "x11") ||
                                     !iCmpStr(SDL_GetCurrentVideoDriver(), "wayland");
@@ -1309,6 +1371,10 @@ static void init_App_(iApp *d, int argc, char **argv) {
         /* HACK: Force a resize so widgets update their state. */
         resize_MainWindow(d->window, -1, -1);
     }
+#if defined (iPlatformAndroid)
+    /* See if there is something to import from backup. */
+    javaCommand_Android("backup.load");
+#endif
 }
 
 static void deinit_App(iApp *d) {
@@ -1688,6 +1754,11 @@ void processEvents_App(enum iAppEventMode eventMode) {
             case SDL_APP_WILLENTERBACKGROUND:
 #if defined (iPlatformAppleMobile)
                 updateNowPlayingInfo_iOS();
+#endif
+#if defined (iPlatformAndroidMobile)
+                postCommand_App("backup.now"); /* ensure there's a copy of user data */
+                saveIdentities_GmCerts(certs_App());
+                save_Bookmarks(d->bookmarks, dataDir_App_());
 #endif
                 setFreezeDraw_MainWindow(d->window, iTrue);
                 savePrefs_App_(d);
@@ -2260,10 +2331,12 @@ void postCommand_Root(iRoot *d, const char *command) {
     SDL_PushEvent(&ev);
     iWindow *win = d ? d->window : NULL;
 #if defined (iPlatformAndroid)
-    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s[command] {%d} %s",
-                app_.isLoadingPrefs ? "[Prefs] " : "",
-                (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2),
-                command);
+    if (!startsWith_CStr(command, "backup.")) {
+        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s[command] {%d} %s",
+                    app_.isLoadingPrefs ? "[Prefs] " : "",
+                    (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2),
+                    command);
+    }
 #else
     if (app_.commandEcho) {
         const int windowIndex =
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.15/cdiff/4f55dd4a701ff18885c34b4d0939ea2bdd602dce
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
73.325112 milliseconds
Gemini-to-HTML Time
0.383502 milliseconds

This content has been proxied by September (ba2dc).