1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko.push; 7 8 import android.content.Context; 9 import android.support.annotation.NonNull; 10 import android.support.annotation.WorkerThread; 11 import android.support.v4.util.AtomicFile; 12 import android.util.Log; 13 14 import org.json.JSONException; 15 import org.json.JSONObject; 16 17 import java.io.File; 18 import java.io.FileNotFoundException; 19 import java.io.FileOutputStream; 20 import java.io.IOException; 21 import java.util.HashMap; 22 import java.util.Iterator; 23 import java.util.Map; 24 25 /** 26 * Firefox for Android maintains an App-wide mapping associating 27 * profile names to push registrations. Each push registration in turn associates channels to 28 * push subscriptions. 29 * <p/> 30 * We use a simple storage model of JSON backed by an atomic file. It is assumed that instances 31 * of this class will reference distinct files on disk; and that all accesses will be happen on a 32 * single (worker thread). 33 */ 34 public class PushState { 35 private static final String LOG_TAG = "GeckoPushState"; 36 37 private static final long VERSION = 1L; 38 39 protected final @NonNull AtomicFile file; 40 41 protected final @NonNull Map<String, PushRegistration> registrations; 42 PushState(Context context, @NonNull String fileName)43 public PushState(Context context, @NonNull String fileName) { 44 this.registrations = new HashMap<>(); 45 46 file = new AtomicFile(new File(context.getApplicationInfo().dataDir, fileName)); 47 synchronized (file) { 48 try { 49 final String s = new String(file.readFully(), "UTF-8"); 50 final JSONObject temp = new JSONObject(s); 51 if (temp.optLong("version", 0L) != VERSION) { 52 throw new JSONException("Unknown version!"); 53 } 54 55 final JSONObject registrationsObject = temp.getJSONObject("registrations"); 56 final Iterator<String> it = registrationsObject.keys(); 57 while (it.hasNext()) { 58 final String profileName = it.next(); 59 final PushRegistration registration = PushRegistration.fromJSONObject(registrationsObject.getJSONObject(profileName)); 60 this.registrations.put(profileName, registration); 61 } 62 } catch (FileNotFoundException e) { 63 Log.i(LOG_TAG, "No storage found; starting fresh."); 64 this.registrations.clear(); 65 } catch (IOException | JSONException e) { 66 Log.w(LOG_TAG, "Got exception reading storage; dropping storage and starting fresh.", e); 67 this.registrations.clear(); 68 } 69 } 70 } 71 toJSONObject()72 public JSONObject toJSONObject() throws JSONException { 73 final JSONObject registrations = new JSONObject(); 74 for (Map.Entry<String, PushRegistration> entry : this.registrations.entrySet()) { 75 registrations.put(entry.getKey(), entry.getValue().toJSONObject()); 76 } 77 78 final JSONObject jsonObject = new JSONObject(); 79 jsonObject.put("version", 1L); 80 jsonObject.put("registrations", registrations); 81 return jsonObject; 82 } 83 84 /** 85 * Synchronously persist the cache to disk. 86 * @return whether the cache was persisted successfully. 87 */ 88 @WorkerThread checkpoint()89 public boolean checkpoint() { 90 synchronized (file) { 91 FileOutputStream fileOutputStream = null; 92 try { 93 fileOutputStream = file.startWrite(); 94 fileOutputStream.write(toJSONObject().toString().getBytes("UTF-8")); 95 file.finishWrite(fileOutputStream); 96 return true; 97 } catch (JSONException | IOException e) { 98 Log.e(LOG_TAG, "Got exception writing JSON storage; ignoring.", e); 99 if (fileOutputStream != null) { 100 file.failWrite(fileOutputStream); 101 } 102 return false; 103 } 104 } 105 } 106 putRegistration(@onNull String profileName, @NonNull PushRegistration registration)107 public PushRegistration putRegistration(@NonNull String profileName, @NonNull PushRegistration registration) { 108 return registrations.put(profileName, registration); 109 } 110 111 /** 112 * Return the existing push registration for the given profile name. 113 * @return the push registration, if one is registered; null otherwise. 114 */ getRegistration(@onNull String profileName)115 public PushRegistration getRegistration(@NonNull String profileName) { 116 return registrations.get(profileName); 117 } 118 119 /** 120 * Return all push registrations, keyed by profile names. 121 * @return a map of all push registrations. <b>The map is intentionally mutable - be careful!</b> 122 */ getRegistrations()123 public @NonNull Map<String, PushRegistration> getRegistrations() { 124 return registrations; 125 } 126 127 /** 128 * Remove any existing push registration for the given profile name. 129 * </p> 130 * Most registration removals are during iteration, which should use an iterator that is 131 * aware of removals. 132 * @return the removed push registration, if one was removed; null otherwise. 133 */ removeRegistration(@onNull String profileName)134 public PushRegistration removeRegistration(@NonNull String profileName) { 135 return registrations.remove(profileName); 136 } 137 } 138