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