diff options
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/push/PushState.java')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/push/PushState.java | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushState.java b/mobile/android/base/java/org/mozilla/gecko/push/PushState.java new file mode 100644 index 000000000..686bf5a0d --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/push/PushState.java @@ -0,0 +1,137 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.push; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import android.support.v4.util.AtomicFile; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Firefox for Android maintains an App-wide mapping associating + * profile names to push registrations. Each push registration in turn associates channels to + * push subscriptions. + * <p/> + * We use a simple storage model of JSON backed by an atomic file. It is assumed that instances + * of this class will reference distinct files on disk; and that all accesses will be happen on a + * single (worker thread). + */ +public class PushState { + private static final String LOG_TAG = "GeckoPushState"; + + private static final long VERSION = 1L; + + protected final @NonNull AtomicFile file; + + protected final @NonNull Map<String, PushRegistration> registrations; + + public PushState(Context context, @NonNull String fileName) { + this.registrations = new HashMap<>(); + + file = new AtomicFile(new File(context.getApplicationInfo().dataDir, fileName)); + synchronized (file) { + try { + final String s = new String(file.readFully(), "UTF-8"); + final JSONObject temp = new JSONObject(s); + if (temp.optLong("version", 0L) != VERSION) { + throw new JSONException("Unknown version!"); + } + + final JSONObject registrationsObject = temp.getJSONObject("registrations"); + final Iterator<String> it = registrationsObject.keys(); + while (it.hasNext()) { + final String profileName = it.next(); + final PushRegistration registration = PushRegistration.fromJSONObject(registrationsObject.getJSONObject(profileName)); + this.registrations.put(profileName, registration); + } + } catch (FileNotFoundException e) { + Log.i(LOG_TAG, "No storage found; starting fresh."); + this.registrations.clear(); + } catch (IOException | JSONException e) { + Log.w(LOG_TAG, "Got exception reading storage; dropping storage and starting fresh.", e); + this.registrations.clear(); + } + } + } + + public JSONObject toJSONObject() throws JSONException { + final JSONObject registrations = new JSONObject(); + for (Map.Entry<String, PushRegistration> entry : this.registrations.entrySet()) { + registrations.put(entry.getKey(), entry.getValue().toJSONObject()); + } + + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("version", 1L); + jsonObject.put("registrations", registrations); + return jsonObject; + } + + /** + * Synchronously persist the cache to disk. + * @return whether the cache was persisted successfully. + */ + @WorkerThread + public boolean checkpoint() { + synchronized (file) { + FileOutputStream fileOutputStream = null; + try { + fileOutputStream = file.startWrite(); + fileOutputStream.write(toJSONObject().toString().getBytes("UTF-8")); + file.finishWrite(fileOutputStream); + return true; + } catch (JSONException | IOException e) { + Log.e(LOG_TAG, "Got exception writing JSON storage; ignoring.", e); + if (fileOutputStream != null) { + file.failWrite(fileOutputStream); + } + return false; + } + } + } + + public PushRegistration putRegistration(@NonNull String profileName, @NonNull PushRegistration registration) { + return registrations.put(profileName, registration); + } + + /** + * Return the existing push registration for the given profile name. + * @return the push registration, if one is registered; null otherwise. + */ + public PushRegistration getRegistration(@NonNull String profileName) { + return registrations.get(profileName); + } + + /** + * Return all push registrations, keyed by profile names. + * @return a map of all push registrations. <b>The map is intentionally mutable - be careful!</b> + */ + public @NonNull Map<String, PushRegistration> getRegistrations() { + return registrations; + } + + /** + * Remove any existing push registration for the given profile name. + * </p> + * Most registration removals are during iteration, which should use an iterator that is + * aware of removals. + * @return the removed push registration, if one was removed; null otherwise. + */ + public PushRegistration removeRegistration(@NonNull String profileName) { + return registrations.remove(profileName); + } +} |