Synchronized localstorage for android WebViews with online and offline content

1.2k views Asked by At

I have an app that allows users to play games online or cache them locally. When they convert their game to cached, I don't want them to lose progress.

Prior to KitKat, you could set the localstorage dir, but I need newer phones to support this feature.

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    playingField.getSettings().setDatabasePath("/data/data/" + playingField.getContext().getPackageName() + "/databases/");
}

I can't find a replacement method. As a result, here are the two directories:

root@ghost:/data/data/com.myapp.android/app_webview/Local Storage #
ls -la
total 80
drwx------ 2 u0_a165 u0_a165 4096 2017-01-06 11:51 .
drwxrwx--x 4 u0_a165 u0_a165 4096 2017-01-06 11:41 ..
-rw------- 1 u0_a165 u0_a165 4096 2017-01-06 11:51 file__0.localstorage
-rw------- 1 u0_a165 u0_a165    0 2017-01-06 11:51 file__0.localstorage-journal
-rw------- 1 u0_a165 u0_a165 4096 2017-01-06 12:02 http_cdn.myapp.com_0.localstorage
-rw------- 1 u0_a165 u0_a165    0 2017-01-06 12:02 http_cdn.myapp.com_0.localstorage-journal

Has anyone solved this issue?

I may try synchronizing the files during the onPause() of the activity, but I would like to know if there is a more elegant solution - like consolidated storage.

2

There are 2 answers

0
copolii On BEST ANSWER

This feels a bit dirty, but it works (on KitKat and newer)!

The LocalStorageCopier class below uses a SharedPreferences store to keep track of the items (feel free to use whatever else you want instead). Before you load the url (local or remote) create an instance of the LocalStorageCopier and add it as a JavascriptInterface to your webView:

webView.setWebViewClient(new WebClient());
storageBackup = new LocalStorageCopier(someContext, someId);
webView.addJavascriptInterface(storageBackup, "backup");
webView.loadUrl (someUrl);

Note: it is important that the javascript interface is added before loading the page. I was scratching my head as to why "backup is undefined" until I moved this bit to before loading the url (I was adding it just prior to performing the backup).

Now you just have to tie in the backup and restore actions.

Backup: As you mentioned above, the onPause method is a good place to back this stuff up. At an appropriate place within your onPause add the code below:

webView.evaluateJavascript(BACKUP_JAVASCRIPT, new ValueCallback<String>() {
        @Override public void onReceiveValue(String s) {
            LOG.d("Backed up.");
        }
    });

where BACKUP_JAVASCRIPT is "(function() { console.log('backing up'); backup.clear(); for (var key in localStorage) { backup.set(key, localStorage.getItem(key)); }})()"

So now every time your activity is paused, your localStorage is backed up.

Restore is done in a very similar fashion. You need to initiate the restore action after the page is loaded. This is where the WebClient class (code below) comes in. Once the page is done loading, you have to grab all the items in your LocalStorageCopier instance and put them in the localStorage. The javascript for RESTORE_JAVASCRIPT is: "(function(){console.log('Restoring'); backup.dump(); var len = backup.size(); for (i = 0; i < len; i++) { var key = backup.key(i); console.log(key); localStorage.setItem(key, backup.get(key));}})()"

LocalStorageCopier

public static class LocalStorageCopier {
    private final SharedPreferences store;
    private String[] keys = null;
    private String[] values = null;

    private boolean dumped = false;

    LocalStorageCopier(final Context context, final String id) {
        store = context.getSharedPreferences(id, Context.MODE_PRIVATE);
    }

    @JavascriptInterface
    public synchronized void dump() {
        if (dumped) throw new IllegalStateException("already dumped");
        final Map<String, ?> map = store.getAll();
        final int size = map.size();
        keys = new String[size];
        values = new String[size];

        int cur = 0;

        for (final String key : map.keySet()) {
            keys[cur] = key;
            values[cur] = (String) map.get(key);
            ++cur;
        }

        dumped = true;
    }

    @JavascriptInterface
    public synchronized int size() {
        if (!dumped) throw new IllegalStateException("dump() first");
        return keys.length;
    }

    @JavascriptInterface
    public synchronized String key(final int i) {
        if (!dumped) throw new IllegalStateException("dump() first");
        return keys[i];
    }

    @JavascriptInterface
    public synchronized String value(final int i) {
        if (!dumped) throw new IllegalStateException("dump() first");
        return values[i];
    }

    @JavascriptInterface
    public synchronized String get(final String key) {
        return store.getString(key, null);
    }

    @JavascriptInterface
    public synchronized void set(final String key, final String value) {
        if (dumped) throw new IllegalStateException("already dumped");
        store.edit().putString(key, value).apply();
    }

    @JavascriptInterface
    public synchronized void clear() {
        store.edit().clear().apply();
        keys = null;
        values = null;
        dumped = false;
    }

    @Override public synchronized String toString() {
        return store.getAll().toString();
    }
}

WebClient

class WebClient extends WebViewClient {
    @Override public void onPageFinished(WebView view, String url) {
        LOG.d("Page finished. Restoring storage.");

        view.evaluateJavascript(RESTORE_JAVASCRIPT, new ValueCallback<String>() {
            @Override public void onReceiveValue(String s) {
                LOG.d("Restored.");
            }
        });

        super.onPageFinished(view, url);
    }
}
0
Cory Roy On

Perhaps using a symlink, both versions of the webview would use the same file:

Creating SymLinks in Android