/* -*- 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. *

* 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 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 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 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. The map is intentionally mutable - be careful! */ public @NonNull Map getRegistrations() { return registrations; } /** * Remove any existing push registration for the given profile name. *

* 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); } }