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