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;
7 
8 import org.mozilla.gecko.annotation.RobocopTarget;
9 import org.mozilla.gecko.annotation.WrapForJNI;
10 
11 import android.support.v4.util.SimpleArrayMap;
12 import android.util.Log;
13 import android.util.SparseArray;
14 
15 import java.util.ArrayList;
16 import java.util.HashSet;
17 import java.util.Iterator;
18 import java.util.List;
19 
20 /**
21  * Helper class to get/set gecko prefs.
22  */
23 public final class PrefsHelper {
24     private static final String LOGTAG = "GeckoPrefsHelper";
25 
26     // Map pref name to ArrayList for multiple observers or PrefHandler for single observer.
27     private static final SimpleArrayMap<String, Object> OBSERVERS = new SimpleArrayMap<>();
28     private static final HashSet<String> INT_TO_STRING_PREFS = new HashSet<>(8);
29     private static final HashSet<String> INT_TO_BOOL_PREFS = new HashSet<>(2);
30 
31     static {
32         INT_TO_STRING_PREFS.add("browser.chrome.titlebarMode");
33         INT_TO_STRING_PREFS.add("network.cookie.cookieBehavior");
34         INT_TO_STRING_PREFS.add("home.sync.updateMode");
35         INT_TO_STRING_PREFS.add("browser.image_blocking");
36         INT_TO_BOOL_PREFS.add("browser.display.use_document_fonts");
37     }
38 
39     @WrapForJNI
40     private static final int PREF_INVALID = -1;
41     @WrapForJNI
42     private static final int PREF_FINISH = 0;
43     @WrapForJNI
44     private static final int PREF_BOOL = 1;
45     @WrapForJNI
46     private static final int PREF_INT = 2;
47     @WrapForJNI
48     private static final int PREF_STRING = 3;
49 
50     @WrapForJNI(stubName = "GetPrefs", dispatchTo = "gecko")
nativeGetPrefs(String[] prefNames, PrefHandler handler)51     private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
52     @WrapForJNI(stubName = "SetPref", dispatchTo = "gecko")
nativeSetPref(String prefName, boolean flush, int type, boolean boolVal, int intVal, String strVal)53     private static native void nativeSetPref(String prefName, boolean flush, int type,
54                                              boolean boolVal, int intVal, String strVal);
55     @WrapForJNI(stubName = "AddObserver", dispatchTo = "gecko")
nativeAddObserver(String[] prefNames, PrefHandler handler, String[] prefsToObserve)56     private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
57                                                  String[] prefsToObserve);
58     @WrapForJNI(stubName = "RemoveObserver", dispatchTo = "gecko")
nativeRemoveObserver(String[] prefToUnobserve)59     private static native void nativeRemoveObserver(String[] prefToUnobserve);
60 
61     @RobocopTarget
getPrefs(final String[] prefNames, final PrefHandler callback)62     public static void getPrefs(final String[] prefNames, final PrefHandler callback) {
63         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
64             nativeGetPrefs(prefNames, callback);
65         } else {
66             GeckoThread.queueNativeCallUntil(
67                     GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeGetPrefs",
68                     String[].class, prefNames, PrefHandler.class, callback);
69         }
70     }
71 
getPref(final String prefName, final PrefHandler callback)72     public static void getPref(final String prefName, final PrefHandler callback) {
73         getPrefs(new String[] { prefName }, callback);
74     }
75 
getPrefs(final ArrayList<String> prefNames, final PrefHandler callback)76     public static void getPrefs(final ArrayList<String> prefNames, final PrefHandler callback) {
77         getPrefs(prefNames.toArray(new String[prefNames.size()]), callback);
78     }
79 
80     @RobocopTarget
setPref(final String pref, final Object value, final boolean flush)81     public static void setPref(final String pref, final Object value, final boolean flush) {
82         final int type;
83         boolean boolVal = false;
84         int intVal = 0;
85         String strVal = null;
86 
87         if (INT_TO_STRING_PREFS.contains(pref)) {
88             // When sending to Java, we normalized special preferences that use integers
89             // and strings to represent booleans. Here, we convert them back to their
90             // actual types so we can store them.
91             type = PREF_INT;
92             intVal = Integer.parseInt(String.valueOf(value));
93         } else if (INT_TO_BOOL_PREFS.contains(pref)) {
94             type = PREF_INT;
95             intVal = (Boolean) value ? 1 : 0;
96         } else if (value instanceof Boolean) {
97             type = PREF_BOOL;
98             boolVal = (Boolean) value;
99         } else if (value instanceof Integer) {
100             type = PREF_INT;
101             intVal = (Integer) value;
102         } else {
103             type = PREF_STRING;
104             strVal = String.valueOf(value);
105         }
106 
107         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
108             nativeSetPref(pref, flush, type, boolVal, intVal, strVal);
109         } else {
110             GeckoThread.queueNativeCallUntil(
111                     GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeSetPref",
112                     String.class, pref, flush, type, boolVal, intVal, String.class, strVal);
113         }
114     }
115 
setPref(final String pref, final Object value)116     public static void setPref(final String pref, final Object value) {
117         setPref(pref, value, /* flush */ false);
118     }
119 
120     @RobocopTarget
addObserver(final String[] prefNames, final PrefHandler handler)121     public synchronized static void addObserver(final String[] prefNames,
122                                                 final PrefHandler handler) {
123         List<String> prefsToObserve = null;
124 
125         for (String pref : prefNames) {
126             final Object existing = OBSERVERS.get(pref);
127 
128             if (existing == null) {
129                 // Not observing yet, so add observer.
130                 if (prefsToObserve == null) {
131                     prefsToObserve = new ArrayList<>(prefNames.length);
132                 }
133                 prefsToObserve.add(pref);
134                 OBSERVERS.put(pref, handler);
135 
136             } else if (existing instanceof PrefHandler) {
137                 // Already observing one, so turn it into an array.
138                 final List<PrefHandler> handlerList = new ArrayList<>(2);
139                 handlerList.add((PrefHandler) existing);
140                 handlerList.add(handler);
141                 OBSERVERS.put(pref, handlerList);
142 
143             } else {
144                 // Already observing multiple, so add to existing array.
145                 @SuppressWarnings("unchecked")
146                 final List<PrefHandler> handlerList = (List) existing;
147                 handlerList.add(handler);
148             }
149         }
150 
151         final String[] namesToObserve = prefsToObserve == null ? null :
152                 prefsToObserve.toArray(new String[prefsToObserve.size()]);
153 
154         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
155             nativeAddObserver(prefNames, handler, namesToObserve);
156         } else {
157             GeckoThread.queueNativeCallUntil(
158                     GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeAddObserver",
159                     String[].class, prefNames, PrefHandler.class, handler,
160                     String[].class, namesToObserve);
161         }
162     }
163 
164     @RobocopTarget
removeObserver(final PrefHandler handler)165     public synchronized static void removeObserver(final PrefHandler handler) {
166         List<String> prefsToUnobserve = null;
167 
168         for (int i = OBSERVERS.size() - 1; i >= 0; i--) {
169             final Object existing = OBSERVERS.valueAt(i);
170             boolean removeObserver = false;
171 
172             if (existing == handler) {
173                 removeObserver = true;
174 
175             } else if (!(existing instanceof PrefHandler)) {
176                 // Removing existing handler from list.
177                 @SuppressWarnings("unchecked")
178                 final List<PrefHandler> handlerList = (List) existing;
179                 if (handlerList.remove(handler) && handlerList.isEmpty()) {
180                     removeObserver = true;
181                 }
182             }
183 
184             if (removeObserver) {
185                 // Removed last handler, so remove observer.
186                 if (prefsToUnobserve == null) {
187                     prefsToUnobserve = new ArrayList<>();
188                 }
189                 prefsToUnobserve.add(OBSERVERS.keyAt(i));
190                 OBSERVERS.removeAt(i);
191             }
192         }
193 
194         if (prefsToUnobserve == null) {
195             return;
196         }
197 
198         final String[] namesToUnobserve =
199                 prefsToUnobserve.toArray(new String[prefsToUnobserve.size()]);
200 
201         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
202             nativeRemoveObserver(namesToUnobserve);
203         } else {
204             GeckoThread.queueNativeCallUntil(
205                     GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeRemoveObserver",
206                     String[].class, namesToUnobserve);
207         }
208     }
209 
210     @WrapForJNI(calledFrom = "gecko")
callPrefHandler(final PrefHandler handler, int type, final String pref, boolean boolVal, int intVal, String strVal)211     private static void callPrefHandler(final PrefHandler handler, int type, final String pref,
212                                         boolean boolVal, int intVal, String strVal) {
213 
214         // Some Gecko preferences use integers or strings to reference state instead of
215         // directly representing the value.  Since the Java UI uses the type to determine
216         // which ui elements to show and how to handle them, we need to normalize these
217         // preferences to the correct type.
218         if (INT_TO_STRING_PREFS.contains(pref)) {
219             type = PREF_STRING;
220             strVal = String.valueOf(intVal);
221         } else if (INT_TO_BOOL_PREFS.contains(pref)) {
222             type = PREF_BOOL;
223             boolVal = intVal == 1;
224         }
225 
226         switch (type) {
227             case PREF_FINISH:
228                 handler.finish();
229                 return;
230             case PREF_BOOL:
231                 handler.prefValue(pref, boolVal);
232                 return;
233             case PREF_INT:
234                 handler.prefValue(pref, intVal);
235                 return;
236             case PREF_STRING:
237                 handler.prefValue(pref, strVal);
238                 return;
239         }
240         throw new IllegalArgumentException();
241     }
242 
243     @WrapForJNI(calledFrom = "gecko")
onPrefChange(final String pref, final int type, final boolean boolVal, final int intVal, final String strVal)244     private synchronized static void onPrefChange(final String pref, final int type,
245                                                   final boolean boolVal, final int intVal,
246                                                   final String strVal) {
247         final Object existing = OBSERVERS.get(pref);
248 
249         if (existing == null) {
250             return;
251         }
252 
253         final Iterator<PrefHandler> itor;
254         PrefHandler handler;
255 
256         if (existing instanceof PrefHandler) {
257             itor = null;
258             handler = (PrefHandler) existing;
259         } else {
260             @SuppressWarnings("unchecked")
261             final List<PrefHandler> handlerList = (List) existing;
262             if (handlerList.isEmpty()) {
263                 return;
264             }
265             itor = handlerList.iterator();
266             handler = itor.next();
267         }
268 
269         do {
270             callPrefHandler(handler, type, pref, boolVal, intVal, strVal);
271             handler.finish();
272 
273             handler = itor != null && itor.hasNext() ? itor.next() : null;
274         } while (handler != null);
275     }
276 
277     public interface PrefHandler {
prefValue(String pref, boolean value)278         void prefValue(String pref, boolean value);
prefValue(String pref, int value)279         void prefValue(String pref, int value);
prefValue(String pref, String value)280         void prefValue(String pref, String value);
finish()281         void finish();
282     }
283 
284     public static abstract class PrefHandlerBase implements PrefHandler {
285         @Override
prefValue(String pref, boolean value)286         public void prefValue(String pref, boolean value) {
287             throw new UnsupportedOperationException(
288                     "Unhandled boolean pref " + pref + "; wrong type?");
289         }
290 
291         @Override
prefValue(String pref, int value)292         public void prefValue(String pref, int value) {
293             throw new UnsupportedOperationException(
294                     "Unhandled int pref " + pref + "; wrong type?");
295         }
296 
297         @Override
prefValue(String pref, String value)298         public void prefValue(String pref, String value) {
299             throw new UnsupportedOperationException(
300                     "Unhandled String pref " + pref + "; wrong type?");
301         }
302 
303         @Override
finish()304         public void finish() {
305         }
306     }
307 }
308