1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.sync.notifier;
6
7import android.accounts.Account;
8import android.content.Context;
9import android.content.SharedPreferences;
10import android.preference.PreferenceManager;
11import android.util.Base64;
12import android.util.Log;
13
14import com.google.ipc.invalidation.external.client.types.ObjectId;
15
16import org.chromium.base.VisibleForTesting;
17
18import java.util.Collection;
19import java.util.HashSet;
20import java.util.Set;
21
22import javax.annotation.Nullable;
23
24/**
25 * Class to manage the preferences used by the invalidation client.
26 * <p>
27 * This class provides methods to read and write the preferences used by the invalidation client.
28 * <p>
29 * To read a preference, call the appropriate {@code get...} method.
30 * <p>
31 * To write a preference, first call {@link #edit} to obtain a {@link EditContext}. Then, make
32 * one or more calls to a {@code set...} method, providing the same edit context to each call.
33 * Finally, call {@link #commit(EditContext)} to save the changes to stable storage.
34 *
35 * @author dsmyers@google.com (Daniel Myers)
36 */
37public class InvalidationPreferences {
38    /**
39     * Wrapper around a {@link android.content.SharedPreferences.Editor} for the preferences.
40     * Used to avoid exposing raw preference objects to users of this class.
41     */
42    public class EditContext {
43        private final SharedPreferences.Editor mEditor;
44
45        EditContext() {
46            mEditor = PreferenceManager.getDefaultSharedPreferences(mContext).edit();
47        }
48    }
49
50    @VisibleForTesting
51    public static class PrefKeys {
52        /**
53         * Shared preference key to store the invalidation types that we want to register
54         * for.
55         */
56        @VisibleForTesting
57        public static final String SYNC_TANGO_TYPES = "sync_tango_types";
58
59        /**
60         * Shared preference key to store tango object ids for additional objects that we want to
61         * register for.
62         */
63        @VisibleForTesting
64        public static final String TANGO_OBJECT_IDS = "tango_object_ids";
65
66        /** Shared preference key to store the name of the account in use. */
67        @VisibleForTesting
68        public static final String SYNC_ACCT_NAME = "sync_acct_name";
69
70        /** Shared preference key to store the type of account in use. */
71        static final String SYNC_ACCT_TYPE = "sync_acct_type";
72
73        /** Shared preference key to store internal notification client library state. */
74        static final String SYNC_TANGO_INTERNAL_STATE = "sync_tango_internal_state";
75    }
76
77    private static final String TAG = "InvalidationPreferences";
78
79    private final Context mContext;
80
81    public InvalidationPreferences(Context context) {
82        Context appContext = context.getApplicationContext();
83        if (appContext == null) throw new NullPointerException("Unable to get application context");
84        mContext = appContext;
85    }
86
87    /** Returns a new {@link EditContext} to modify the preferences managed by this class. */
88    public EditContext edit() {
89        return new EditContext();
90    }
91
92    /**
93     * Applies the changes accumulated in {@code editContext}. Returns whether they were
94     * successfully written.
95     * <p>
96     * NOTE: this method performs blocking I/O and must not be called from the UI thread.
97     */
98    public boolean commit(EditContext editContext) {
99        if (!editContext.mEditor.commit()) {
100            Log.w(TAG, "Failed to commit invalidation preferences");
101            return false;
102        }
103        return true;
104    }
105
106    /** Returns the saved sync types, or {@code null} if none exist. */
107    @Nullable public Set<String> getSavedSyncedTypes() {
108        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
109        return preferences.getStringSet(PrefKeys.SYNC_TANGO_TYPES, null);
110    }
111
112    /** Sets the saved sync types to {@code syncTypes} in {@code editContext}. */
113    public void setSyncTypes(EditContext editContext, Collection<String> syncTypes) {
114        if (syncTypes == null) throw new NullPointerException("syncTypes is null.");
115        Set<String> selectedTypesSet = new HashSet<String>(syncTypes);
116        editContext.mEditor.putStringSet(PrefKeys.SYNC_TANGO_TYPES, selectedTypesSet);
117    }
118
119    /** Returns the saved non-sync object ids, or {@code null} if none exist. */
120    @Nullable
121    public Set<ObjectId> getSavedObjectIds() {
122        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
123        Set<String> objectIdStrings = preferences.getStringSet(PrefKeys.TANGO_OBJECT_IDS, null);
124        if (objectIdStrings == null) {
125            return null;
126        }
127        Set<ObjectId> objectIds = new HashSet<ObjectId>(objectIdStrings.size());
128        for (String objectIdString : objectIdStrings) {
129            ObjectId objectId = getObjectId(objectIdString);
130            if (objectId != null) {
131                objectIds.add(objectId);
132            }
133        }
134        return objectIds;
135    }
136
137    /** Sets the saved non-sync object ids */
138    public void setObjectIds(EditContext editContext, Collection<ObjectId> objectIds) {
139        if (objectIds == null) throw new NullPointerException("objectIds is null.");
140        Set<String> objectIdStrings = new HashSet<String>(objectIds.size());
141        for (ObjectId objectId : objectIds) {
142            objectIdStrings.add(getObjectIdString(objectId));
143        }
144        editContext.mEditor.putStringSet(PrefKeys.TANGO_OBJECT_IDS, objectIdStrings);
145    }
146
147    /** Returns the saved account, or {@code null} if none exists. */
148    @Nullable public Account getSavedSyncedAccount() {
149        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
150        String accountName = preferences.getString(PrefKeys.SYNC_ACCT_NAME, null);
151        String accountType = preferences.getString(PrefKeys.SYNC_ACCT_TYPE, null);
152        if (accountName == null || accountType == null) {
153            return null;
154        }
155        return new Account(accountName, accountType);
156    }
157
158    /** Sets the saved account to {@code account} in {@code editContext}. */
159    public void setAccount(EditContext editContext, Account account) {
160        editContext.mEditor.putString(PrefKeys.SYNC_ACCT_NAME, account.name);
161        editContext.mEditor.putString(PrefKeys.SYNC_ACCT_TYPE, account.type);
162    }
163
164    /** Returns the notification client internal state. */
165    @Nullable public byte[] getInternalNotificationClientState() {
166        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
167        String base64State = preferences.getString(PrefKeys.SYNC_TANGO_INTERNAL_STATE, null);
168        if (base64State == null) {
169            return null;
170        }
171        return Base64.decode(base64State, Base64.DEFAULT);
172    }
173
174    /** Sets the notification client internal state to {@code state}. */
175    public void setInternalNotificationClientState(EditContext editContext, byte[] state) {
176        editContext.mEditor.putString(PrefKeys.SYNC_TANGO_INTERNAL_STATE,
177                Base64.encodeToString(state, Base64.DEFAULT));
178    }
179
180    /** Converts the given object id to a string for storage in preferences. */
181    private String getObjectIdString(ObjectId objectId) {
182        return objectId.getSource() + ":" + new String(objectId.getName());
183    }
184
185    /**
186     * Converts the given object id string stored in preferences to an object id.
187     * Returns null if the string does not represent a valid object id.
188     */
189    private ObjectId getObjectId(String objectIdString) {
190        int separatorPos = objectIdString.indexOf(':');
191        // Ensure that the separator is surrounded by at least one character on each side.
192        if (separatorPos < 1 || separatorPos == objectIdString.length() - 1) {
193            return null;
194        }
195        int objectSource;
196        try {
197            objectSource = Integer.parseInt(objectIdString.substring(0, separatorPos));
198        } catch (NumberFormatException e) {
199            return null;
200        }
201        byte[] objectName = objectIdString.substring(separatorPos + 1).getBytes();
202        return ObjectId.newInstance(objectSource, objectName);
203    }
204}
205