summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/push/PushState.java
blob: 686bf5a0d1e343fa0a1d6bb455a8c8960cae48c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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);
    }
}