1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.util;
7 
8 import org.mozilla.gecko.annotation.RobocopTarget;
9 import org.mozilla.gecko.annotation.WrapForJNI;
10 
11 import org.json.JSONArray;
12 import org.json.JSONException;
13 import org.json.JSONObject;
14 
15 import android.os.Build;
16 import android.os.Bundle;
17 import android.os.Parcel;
18 import android.os.Parcelable;
19 import androidx.collection.SimpleArrayMap;
20 
21 import java.lang.reflect.Array;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Iterator;
25 
26 /**
27  * A lighter-weight version of Bundle that adds support for type coercion (e.g.
28  * int to double) in order to better cooperate with JS objects.
29  */
30 @RobocopTarget
31 public final class GeckoBundle implements Parcelable {
32     private static final String LOGTAG = "GeckoBundle";
33     private static final boolean DEBUG = false;
34 
35     @WrapForJNI(calledFrom = "gecko")
36     private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
37     private static final int[] EMPTY_INT_ARRAY = new int[0];
38     private static final long[] EMPTY_LONG_ARRAY = new long[0];
39     private static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
40     private static final String[] EMPTY_STRING_ARRAY = new String[0];
41     private static final GeckoBundle[] EMPTY_BUNDLE_ARRAY = new GeckoBundle[0];
42 
43     private SimpleArrayMap<String, Object> mMap;
44 
45     /**
46      * Construct an empty GeckoBundle.
47      */
GeckoBundle()48     public GeckoBundle() {
49         mMap = new SimpleArrayMap<>();
50     }
51 
52     /**
53      * Construct an empty GeckoBundle with specific capacity.
54      *
55      * @param capacity Initial capacity.
56      */
GeckoBundle(final int capacity)57     public GeckoBundle(final int capacity) {
58         mMap = new SimpleArrayMap<>(capacity);
59     }
60 
61     /**
62      * Construct a copy of another GeckoBundle.
63      *
64      * @param bundle GeckoBundle to copy from.
65      */
GeckoBundle(final GeckoBundle bundle)66     public GeckoBundle(final GeckoBundle bundle) {
67         mMap = new SimpleArrayMap<>(bundle.mMap);
68     }
69 
70     @WrapForJNI(calledFrom = "gecko")
GeckoBundle(final String[] keys, final Object[] values)71     private GeckoBundle(final String[] keys, final Object[] values) {
72         final int len = keys.length;
73         mMap = new SimpleArrayMap<>(len);
74         for (int i = 0; i < len; i++) {
75             mMap.put(keys[i], values[i]);
76         }
77     }
78 
79     /**
80      * Clear all mappings.
81      */
clear()82     public void clear() {
83         mMap.clear();
84     }
85 
86     /**
87      * Returns whether a mapping exists. Null String, Bundle, or arrays are treated as
88      * nonexistent.
89      *
90      * @param key Key to look for.
91      * @return True if the specified key exists and the value is not null.
92      */
containsKey(final String key)93     public boolean containsKey(final String key) {
94         return mMap.get(key) != null;
95     }
96 
97     /**
98      * Returns the value associated with a mapping as an Object.
99      *
100      * @param key Key to look for.
101      * @return Mapping value or null if the mapping does not exist.
102      */
get(final String key)103     public Object get(final String key) {
104         return mMap.get(key);
105     }
106 
107     /**
108      * Returns the value associated with a boolean mapping, or defaultValue if the mapping
109      * does not exist.
110      *
111      * @param key Key to look for.
112      * @param defaultValue Value to return if mapping does not exist.
113      * @return Boolean value
114      */
getBoolean(final String key, final boolean defaultValue)115     public boolean getBoolean(final String key, final boolean defaultValue) {
116         final Object value = mMap.get(key);
117         return value == null ? defaultValue : (Boolean) value;
118     }
119 
120     /**
121      * Returns the value associated with a boolean mapping, or false if the mapping does
122      * not exist.
123      *
124      * @param key Key to look for.
125      * @return Boolean value
126      */
getBoolean(final String key)127     public boolean getBoolean(final String key) {
128         return getBoolean(key, false);
129     }
130 
131     /**
132      * Returns the value associated with a Boolean mapping, or defaultValue if the mapping
133      * does not exist.
134      *
135      * @param key Key to look for.
136      * @param defaultValue Value to return if mapping does not exist.
137      * @return Boolean value
138      */
getBooleanObject(final String key, final Boolean defaultValue)139     public Boolean getBooleanObject(final String key, final Boolean defaultValue) {
140         final Object value = mMap.get(key);
141         return value == null ? defaultValue : (Boolean) value;
142     }
143 
144     /**
145      * Returns the value associated with a Boolean mapping, or null if the mapping does
146      * not exist.
147      *
148      * @param key Key to look for.
149      * @return Boolean value
150      */
getBooleanObject(final String key)151     public Boolean getBooleanObject(final String key) {
152         return getBooleanObject(key, null);
153     }
154 
155     /**
156      * Returns the value associated with a boolean array mapping, or null if the mapping
157      * does not exist.
158      *
159      * @param key Key to look for.
160      * @return Boolean array value
161      */
getBooleanArray(final String key)162     public boolean[] getBooleanArray(final String key) {
163         final Object value = mMap.get(key);
164         return value == null ? null :
165                 Array.getLength(value) == 0 ? EMPTY_BOOLEAN_ARRAY : (boolean[]) value;
166     }
167 
168     /**
169      * Returns the value associated with a double mapping, or defaultValue if the mapping
170      * does not exist.
171      *
172      * @param key Key to look for.
173      * @param defaultValue Value to return if mapping does not exist.
174      * @return Double value
175      */
getDouble(final String key, final double defaultValue)176     public double getDouble(final String key, final double defaultValue) {
177         final Object value = mMap.get(key);
178         return value == null ? defaultValue : ((Number) value).doubleValue();
179     }
180 
181     /**
182      * Returns the value associated with a double mapping, or 0.0 if the mapping does not
183      * exist.
184      *
185      * @param key Key to look for.
186      * @return Double value
187      */
getDouble(final String key)188     public double getDouble(final String key) {
189         return getDouble(key, 0.0);
190     }
191 
getDoubleArray(final int[] array)192     private static double[] getDoubleArray(final int[] array) {
193         final int len = array.length;
194         final double[] ret = new double[len];
195         for (int i = 0; i < len; i++) {
196             ret[i] = (double) array[i];
197         }
198         return ret;
199     }
200 
201     /**
202      * Returns the value associated with a double array mapping, or null if the mapping
203      * does not exist.
204      *
205      * @param key Key to look for.
206      * @return Double array value
207      */
getDoubleArray(final String key)208     public double[] getDoubleArray(final String key) {
209         final Object value = mMap.get(key);
210         return value == null ? null : Array.getLength(value) == 0 ? EMPTY_DOUBLE_ARRAY :
211                value instanceof int[] ? getDoubleArray((int[]) value) : (double[]) value;
212     }
213 
214     /**
215      * Returns the value associated with an int mapping, or defaultValue if the mapping
216      * does not exist.
217      *
218      * @param key Key to look for.
219      * @param defaultValue Value to return if mapping does not exist.
220      * @return Int value
221      */
getInt(final String key, final int defaultValue)222     public int getInt(final String key, final int defaultValue) {
223         final Object value = mMap.get(key);
224         return value == null ? defaultValue : ((Number) value).intValue();
225     }
226 
227     /**
228      * Returns the value associated with an int mapping, or 0 if the mapping does not
229      * exist.
230      *
231      * @param key Key to look for.
232      * @return Int value
233      */
getInt(final String key)234     public int getInt(final String key) {
235         return getInt(key, 0);
236     }
237 
238     /**
239      * Returns the value associated with an Integer mapping, or defaultValue if the mapping
240      * does not exist.
241      *
242      * @param key Key to look for.
243      * @param defaultValue Value to return if mapping does not exist.
244      * @return Int value
245      */
getInteger(final String key, final Integer defaultValue)246     public Integer getInteger(final String key, final Integer defaultValue) {
247         final Object value = mMap.get(key);
248         return value == null ? defaultValue : ((Integer) value);
249     }
250 
251     /**
252      * Returns the value associated with an Integer mapping, or null if the mapping does not
253      * exist.
254      *
255      * @param key Key to look for.
256      * @return Int value
257      */
getInteger(final String key)258     public Integer getInteger(final String key) {
259         return getInteger(key, null);
260     }
261 
getIntArray(final double[] array)262     private static int[] getIntArray(final double[] array) {
263         final int len = array.length;
264         final int[] ret = new int[len];
265         for (int i = 0; i < len; i++) {
266             ret[i] = (int) array[i];
267         }
268         return ret;
269     }
270 
271     /**
272      * Returns the value associated with an int array mapping, or null if the mapping does
273      * not exist.
274      *
275      * @param key Key to look for.
276      * @return Int array value
277      */
getIntArray(final String key)278     public int[] getIntArray(final String key) {
279         final Object value = mMap.get(key);
280         return value == null ? null : Array.getLength(value) == 0 ? EMPTY_INT_ARRAY :
281                value instanceof double[] ? getIntArray((double[]) value) : (int[]) value;
282     }
283 
284     /**
285      * Returns the value associated with an int/double mapping as a long value, or
286      * defaultValue if the mapping does not exist.
287      *
288      * @param key Key to look for.
289      * @param defaultValue Value to return if mapping does not exist.
290      * @return Long value
291      */
getLong(final String key, final long defaultValue)292     public long getLong(final String key, final long defaultValue) {
293         final Object value = mMap.get(key);
294         return value == null ? defaultValue : ((Number) value).longValue();
295     }
296 
297     /**
298      * Returns the value associated with an int/double mapping as a long value, or
299      * 0 if the mapping does not exist.
300      *
301      * @param key Key to look for.
302      * @return Long value
303      */
getLong(final String key)304     public long getLong(final String key) {
305         return getLong(key, 0L);
306     }
307 
getLongArray(final Object array)308     private static long[] getLongArray(final Object array) {
309         final int len = Array.getLength(array);
310         final long[] ret = new long[len];
311         for (int i = 0; i < len; i++) {
312             ret[i] = ((Number) Array.get(array, i)).longValue();
313         }
314         return ret;
315     }
316 
317     /**
318      * Returns the value associated with an int/double array mapping as a long array, or
319      * null if the mapping does not exist.
320      *
321      * @param key Key to look for.
322      * @return Long array value
323      */
getLongArray(final String key)324     public long[] getLongArray(final String key) {
325         final Object value = mMap.get(key);
326         return value == null ? null :
327                Array.getLength(value) == 0 ? EMPTY_LONG_ARRAY : getLongArray(value);
328     }
329 
330     /**
331      * Returns the value associated with a String mapping, or defaultValue if the mapping
332      * does not exist.
333      *
334      * @param key Key to look for.
335      * @param defaultValue Value to return if mapping value is null or mapping does not exist.
336      * @return String value
337      */
getString(final String key, final String defaultValue)338     public String getString(final String key, final String defaultValue) {
339         // If the key maps to null, technically we should return null because the mapping
340         // exists and null is a valid string value. However, people expect the default
341         // value to be returned instead, so we make an exception to return the default value.
342         final Object value = mMap.get(key);
343         return value == null ? defaultValue : (String) value;
344     }
345 
346     /**
347      * Returns the value associated with a String mapping, or null if the mapping does not
348      * exist.
349      *
350      * @param key Key to look for.
351      * @return String value
352      */
getString(final String key)353     public String getString(final String key) {
354         return getString(key, null);
355     }
356 
357     // The only case where we convert String[] to/from GeckoBundle[] is if every element
358     // is null.
getNullArrayLength(final Object array)359     private static int getNullArrayLength(final Object array) {
360         final int len = Array.getLength(array);
361         for (int i = 0; i < len; i++) {
362             if (Array.get(array, i) != null) {
363                 throw new ClassCastException("Cannot cast array type");
364             }
365         }
366         return len;
367     }
368 
369     /**
370      * Returns the value associated with a String array mapping, or null if the mapping
371      * does not exist.
372      *
373      * @param key Key to look for.
374      * @return String array value
375      */
getStringArray(final String key)376     public String[] getStringArray(final String key) {
377         final Object value = mMap.get(key);
378         return value == null ? null : Array.getLength(value) == 0 ? EMPTY_STRING_ARRAY :
379                !(value instanceof String[]) ? new String[getNullArrayLength(value)] :
380                                               (String[]) value;
381     }
382 
383     /**
384      * Returns the value associated with a GeckoBundle mapping, or null if the mapping
385      * does not exist.
386      *
387      * @param key Key to look for.
388      * @return GeckoBundle value
389      */
getBundle(final String key)390     public GeckoBundle getBundle(final String key) {
391         return (GeckoBundle) mMap.get(key);
392     }
393 
394     /**
395      * Returns the value associated with a GeckoBundle array mapping, or null if the
396      * mapping does not exist.
397      *
398      * @param key Key to look for.
399      * @return GeckoBundle array value
400      */
getBundleArray(final String key)401     public GeckoBundle[] getBundleArray(final String key) {
402         final Object value = mMap.get(key);
403         return value == null ? null : Array.getLength(value) == 0 ? EMPTY_BUNDLE_ARRAY :
404                !(value instanceof GeckoBundle[]) ? new GeckoBundle[getNullArrayLength(value)] :
405                                                    (GeckoBundle[]) value;
406     }
407 
408     /**
409      * Returns whether this GeckoBundle has no mappings.
410      *
411      * @return True if no mapping exists.
412      */
isEmpty()413     public boolean isEmpty() {
414         return mMap.isEmpty();
415     }
416 
417     /**
418      * Returns an array of all mapped keys.
419      *
420      * @return String array containing all mapped keys.
421      */
422     @WrapForJNI(calledFrom = "gecko")
keys()423     public String[] keys() {
424         final int len = mMap.size();
425         final String[] ret = new String[len];
426         for (int i = 0; i < len; i++) {
427             ret[i] = mMap.keyAt(i);
428         }
429         return ret;
430     }
431 
432     @WrapForJNI(calledFrom = "gecko")
values()433     private Object[] values() {
434         final int len = mMap.size();
435         final Object[] ret = new Object[len];
436         for (int i = 0; i < len; i++) {
437             ret[i] = mMap.valueAt(i);
438         }
439         return ret;
440     }
441 
put(final String key, final Object value)442     private void put(final String key, final Object value) {
443         // We intentionally disallow a generic put() method for type safety and sanity. For
444         // example, we assume elsewhere in the code that a value belongs to a small list of
445         // predefined types, and cannot be any arbitrary object. If you want to put an
446         // Object in the bundle, check the type of the Object first and call the
447         // corresponding put methods. For example,
448         //
449         //   if (obj instanceof Integer) {
450         //     bundle.putInt(key, (Integer) key);
451         //   } else if (obj instanceof String) {
452         //     bundle.putString(key, (String) obj);
453         //   } else {
454         //     throw new IllegalArgumentException("unexpected type");
455         //   }
456         throw new UnsupportedOperationException();
457     }
458 
459     /**
460      * Map a key to a boolean value.
461      *
462      * @param key Key to map.
463      * @param value Value to map to.
464      */
putBoolean(final String key, final boolean value)465     public void putBoolean(final String key, final boolean value) {
466         mMap.put(key, value);
467     }
468 
469     /**
470      * Map a key to a boolean array value.
471      *
472      * @param key Key to map.
473      * @param value Value to map to.
474      */
putBooleanArray(final String key, final boolean[] value)475     public void putBooleanArray(final String key, final boolean[] value) {
476         mMap.put(key, value);
477     }
478 
479     /**
480      * Map a key to a boolean array value.
481      *
482      * @param key Key to map.
483      * @param value Value to map to.
484      */
putBooleanArray(final String key, final Boolean[] value)485     public void putBooleanArray(final String key, final Boolean[] value) {
486         if (value == null) {
487             mMap.put(key, null);
488             return;
489         }
490         final boolean[] array = new boolean[value.length];
491         for (int i = 0; i < value.length; i++) {
492             array[i] = value[i];
493         }
494         mMap.put(key, array);
495     }
496 
497     /**
498      * Map a key to a boolean array value.
499      *
500      * @param key Key to map.
501      * @param value Value to map to.
502      */
putBooleanArray(final String key, final Collection<Boolean> value)503     public void putBooleanArray(final String key, final Collection<Boolean> value) {
504         if (value == null) {
505             mMap.put(key, null);
506             return;
507         }
508         final boolean[] array = new boolean[value.size()];
509         int i = 0;
510         for (final Boolean element : value) {
511             array[i++] = element;
512         }
513         mMap.put(key, array);
514     }
515 
516     /**
517      * Map a key to a double value.
518      *
519      * @param key Key to map.
520      * @param value Value to map to.
521      */
putDouble(final String key, final double value)522     public void putDouble(final String key, final double value) {
523         mMap.put(key, value);
524     }
525 
526     /**
527      * Map a key to a double array value.
528      *
529      * @param key Key to map.
530      * @param value Value to map to.
531      */
putDoubleArray(final String key, final double[] value)532     public void putDoubleArray(final String key, final double[] value) {
533         mMap.put(key, value);
534     }
535 
536     /**
537      * Map a key to a double array value.
538      *
539      * @param key Key to map.
540      * @param value Value to map to.
541      */
putDoubleArray(final String key, final Double[] value)542     public void putDoubleArray(final String key, final Double[] value) {
543         putDoubleArray(key, Arrays.asList(value));
544     }
545 
546     /**
547      * Map a key to a double array value.
548      *
549      * @param key Key to map.
550      * @param value Value to map to.
551      */
putDoubleArray(final String key, final Collection<Double> value)552     public void putDoubleArray(final String key, final Collection<Double> value) {
553         if (value == null) {
554             mMap.put(key, null);
555             return;
556         }
557         final double[] array = new double[value.size()];
558         int i = 0;
559         for (final Double element : value) {
560             array[i++] = element;
561         }
562         mMap.put(key, array);
563     }
564 
565     /**
566      * Map a key to an int value.
567      *
568      * @param key Key to map.
569      * @param value Value to map to.
570      */
putInt(final String key, final int value)571     public void putInt(final String key, final int value) {
572         mMap.put(key, value);
573     }
574 
575     /**
576      * Map a key to an int array value.
577      *
578      * @param key Key to map.
579      * @param value Value to map to.
580      */
putIntArray(final String key, final int[] value)581     public void putIntArray(final String key, final int[] value) {
582         mMap.put(key, value);
583     }
584 
585     /**
586      * Map a key to a int array value.
587      *
588      * @param key Key to map.
589      * @param value Value to map to.
590      */
putIntArray(final String key, final Integer[] value)591     public void putIntArray(final String key, final Integer[] value) {
592         putIntArray(key, Arrays.asList(value));
593     }
594 
595     /**
596      * Map a key to a int array value.
597      *
598      * @param key Key to map.
599      * @param value Value to map to.
600      */
putIntArray(final String key, final Collection<Integer> value)601     public void putIntArray(final String key, final Collection<Integer> value) {
602         if (value == null) {
603             mMap.put(key, null);
604             return;
605         }
606         final int[] array = new int[value.size()];
607         int i = 0;
608         for (final Integer element : value) {
609             array[i++] = element;
610         }
611         mMap.put(key, array);
612     }
613 
614     /**
615      * Map a key to a long value stored as a double value.
616      *
617      * @param key Key to map.
618      * @param value Value to map to.
619      */
putLong(final String key, final long value)620     public void putLong(final String key, final long value) {
621         mMap.put(key, (double) value);
622     }
623 
624     /**
625      * Map a key to a long array value stored as a double array value.
626      *
627      * @param key Key to map.
628      * @param value Value to map to.
629      */
putLongArray(final String key, final long[] value)630     public void putLongArray(final String key, final long[] value) {
631         if (value == null) {
632             mMap.put(key, null);
633             return;
634         }
635         final double[] array = new double[value.length];
636         for (int i = 0; i < value.length; i++) {
637             array[i] = (double) value[i];
638         }
639         mMap.put(key, array);
640     }
641 
642     /**
643      * Map a key to a long array value stored as a double array value.
644      *
645      * @param key Key to map.
646      * @param value Value to map to.
647      */
putLongArray(final String key, final Long[] value)648     public void putLongArray(final String key, final Long[] value) {
649         putLongArray(key, Arrays.asList(value));
650     }
651 
652     /**
653      * Map a key to a long array value stored as a double array value.
654      *
655      * @param key Key to map.
656      * @param value Value to map to.
657      */
putLongArray(final String key, final Collection<Long> value)658     public void putLongArray(final String key, final Collection<Long> value) {
659         if (value == null) {
660             mMap.put(key, null);
661             return;
662         }
663         final double[] array = new double[value.size()];
664         int i = 0;
665         for (final Long element : value) {
666             array[i++] = (double) element;
667         }
668         mMap.put(key, array);
669     }
670 
671     /**
672      * Map a key to a String value.
673      *
674      * @param key Key to map.
675      * @param value Value to map to.
676      */
putString(final String key, final String value)677     public void putString(final String key, final String value) {
678         mMap.put(key, value);
679     }
680 
681     /**
682      * Map a key to a String array value.
683      *
684      * @param key Key to map.
685      * @param value Value to map to.
686      */
putStringArray(final String key, final String[] value)687     public void putStringArray(final String key, final String[] value) {
688         mMap.put(key, value);
689     }
690 
691     /**
692      * Map a key to a String array value.
693      *
694      * @param key Key to map.
695      * @param value Value to map to.
696      */
putStringArray(final String key, final Collection<String> value)697     public void putStringArray(final String key, final Collection<String> value) {
698         if (value == null) {
699             mMap.put(key, null);
700             return;
701         }
702         final String[] array = new String[value.size()];
703         int i = 0;
704         for (final String element : value) {
705             array[i++] = element;
706         }
707         mMap.put(key, array);
708     }
709 
710     /**
711      * Map a key to a GeckoBundle value.
712      *
713      * @param key Key to map.
714      * @param value Value to map to.
715      */
putBundle(final String key, final GeckoBundle value)716     public void putBundle(final String key, final GeckoBundle value) {
717         mMap.put(key, value);
718     }
719 
720     /**
721      * Map a key to a GeckoBundle array value.
722      *
723      * @param key Key to map.
724      * @param value Value to map to.
725      */
putBundleArray(final String key, final GeckoBundle[] value)726     public void putBundleArray(final String key, final GeckoBundle[] value) {
727         mMap.put(key, value);
728     }
729 
730     /**
731      * Map a key to a GeckoBundle array value.
732      *
733      * @param key Key to map.
734      * @param value Value to map to.
735      */
putBundleArray(final String key, final Collection<GeckoBundle> value)736     public void putBundleArray(final String key, final Collection<GeckoBundle> value) {
737         if (value == null) {
738             mMap.put(key, null);
739             return;
740         }
741         final GeckoBundle[] array = new GeckoBundle[value.size()];
742         int i = 0;
743         for (final GeckoBundle element : value) {
744             array[i++] = element;
745         }
746         mMap.put(key, array);
747     }
748 
749     /**
750      * Remove a mapping.
751      *
752      * @param key Key to remove.
753      */
remove(final String key)754     public void remove(final String key) {
755         mMap.remove(key);
756     }
757 
758     /**
759      * Returns number of mappings in this GeckoBundle.
760      *
761      * @return Number of mappings.
762      */
size()763     public int size() {
764         return mMap.size();
765     }
766 
normalizeValue(final Object value)767     private static Object normalizeValue(final Object value) {
768         if (value instanceof Integer) {
769             // We treat int and double as the same type.
770             return ((Integer) value).doubleValue();
771 
772         } else if (value instanceof int[]) {
773             // We treat int[] and double[] as the same type.
774             final int[] array = (int[]) value;
775             return array.length == 0 ? EMPTY_STRING_ARRAY : getDoubleArray(array);
776 
777         } else if (value != null && value.getClass().isArray()) {
778             // We treat arrays of all nulls as the same type, including empty arrays.
779             final int len = Array.getLength(value);
780             for (int i = 0; i < len; i++) {
781                 if (Array.get(value, i) != null) {
782                     return value;
783                 }
784             }
785             return len == 0 ? EMPTY_STRING_ARRAY : new String[len];
786         }
787         return value;
788     }
789 
790     @Override // Object
equals(final Object other)791     public boolean equals(final Object other) {
792         if (!(other instanceof GeckoBundle)) {
793             return false;
794         }
795 
796         // Support library's SimpleArrayMap.equals is buggy, so roll our own version.
797         final SimpleArrayMap<String, Object> otherMap = ((GeckoBundle) other).mMap;
798         if (mMap == otherMap) {
799             return true;
800         }
801         if (mMap.size() != otherMap.size()) {
802             return false;
803         }
804 
805         for (int i = 0; i < mMap.size(); i++) {
806             final String thisKey = mMap.keyAt(i);
807             final int otherKey = otherMap.indexOfKey(thisKey);
808             if (otherKey < 0) {
809                 return false;
810             }
811             final Object thisValue = normalizeValue(mMap.valueAt(i));
812             final Object otherValue = normalizeValue(otherMap.valueAt(otherKey));
813             if (thisValue == otherValue) {
814                 continue;
815             } else if (thisValue == null || otherValue == null) {
816                 return false;
817             }
818 
819             final Class<?> thisClass = thisValue.getClass();
820             final Class<?> otherClass = otherValue.getClass();
821             if (thisClass != otherClass && !thisClass.equals(otherClass)) {
822                 return false;
823             } else if (!thisClass.isArray()) {
824                 if (!thisValue.equals(otherValue)) {
825                     return false;
826                 }
827                 continue;
828             }
829 
830             // Work with both primitive arrays and Object arrays, unlike Arrays.equals().
831             final int thisLen = Array.getLength(thisValue);
832             final int otherLen = Array.getLength(otherValue);
833             if (thisLen != otherLen) {
834                 return false;
835             }
836             for (int j = 0; j < thisLen; j++) {
837                 final Object thisElem = Array.get(thisValue, j);
838                 final Object otherElem = Array.get(otherValue, j);
839                 if (thisElem != otherElem && (thisElem == null ||
840                         otherElem == null || !thisElem.equals(otherElem))) {
841                     return false;
842                 }
843             }
844         }
845         return true;
846     }
847 
848     @Override // Object
hashCode()849     public int hashCode() {
850         return mMap.hashCode();
851     }
852 
853     @Override // Object
toString()854     public String toString() {
855         return mMap.toString();
856     }
857 
toJSONObject()858     public JSONObject toJSONObject() throws JSONException {
859         final JSONObject out = new JSONObject();
860         for (int i = 0; i < mMap.size(); i++) {
861             final Object value = mMap.valueAt(i);
862             final Object jsonValue;
863 
864             if (value instanceof GeckoBundle) {
865                 jsonValue = ((GeckoBundle) value).toJSONObject();
866             } else if (value instanceof GeckoBundle[]) {
867                 final GeckoBundle[] array = (GeckoBundle[]) value;
868                 final JSONArray jsonArray = new JSONArray();
869                 for (final GeckoBundle element : array) {
870                     jsonArray.put(element == null ? JSONObject.NULL : element.toJSONObject());
871                 }
872                 jsonValue = jsonArray;
873             } else if (Build.VERSION.SDK_INT >= 19) {
874                 final Object wrapped = JSONObject.wrap(value);
875                 jsonValue = wrapped != null ? wrapped : value.toString();
876             } else if (value == null) {
877                 jsonValue = JSONObject.NULL;
878             } else if (value.getClass().isArray()) {
879                 final JSONArray jsonArray = new JSONArray();
880                 for (int j = 0; j < Array.getLength(value); j++) {
881                     jsonArray.put(Array.get(value, j));
882                 }
883                 jsonValue = jsonArray;
884             } else {
885                 jsonValue = value;
886             }
887             out.put(mMap.keyAt(i), jsonValue);
888         }
889         return out;
890     }
891 
toBundle()892     public Bundle toBundle() {
893         final Bundle out = new Bundle(mMap.size());
894         for (int i = 0; i < mMap.size(); i++) {
895             final String key = mMap.keyAt(i);
896             final Object val = mMap.valueAt(i);
897 
898             if (val == null) {
899                 out.putString(key, null);
900             } else if (val instanceof GeckoBundle) {
901                 out.putBundle(key, ((GeckoBundle) val).toBundle());
902             } else if (val instanceof GeckoBundle[]) {
903                 final GeckoBundle[] array = (GeckoBundle[]) val;
904                 final Parcelable[] parcelables = new Parcelable[array.length];
905                 for (int j = 0; j < array.length; j++) {
906                     if (array[j] != null) {
907                         parcelables[j] = array[j].toBundle();
908                     }
909                 }
910                 out.putParcelableArray(key, parcelables);
911             } else if (val instanceof Boolean) {
912                 out.putBoolean(key, (Boolean) val);
913             } else if (val instanceof boolean[]) {
914                 out.putBooleanArray(key, (boolean[]) val);
915             } else if (val instanceof Byte || val instanceof Short || val instanceof Integer) {
916                 out.putInt(key, ((Number) val).intValue());
917             } else if (val instanceof int[]) {
918                 out.putIntArray(key, (int[]) val);
919             } else if (val instanceof Float || val instanceof Double || val instanceof Long) {
920                 out.putDouble(key, ((Number) val).doubleValue());
921             } else if (val instanceof double[]) {
922                 out.putDoubleArray(key, (double[]) val);
923             } else if (val instanceof CharSequence || val instanceof Character) {
924                 out.putString(key, val.toString());
925             } else if (val instanceof String[]) {
926                 out.putStringArray(key, (String[]) val);
927             } else {
928                 throw new UnsupportedOperationException();
929             }
930         }
931         return out;
932     }
933 
fromBundle(final Bundle bundle)934     public static GeckoBundle fromBundle(final Bundle bundle) {
935         if (bundle == null) {
936             return null;
937         }
938 
939         final String[] keys = new String[bundle.size()];
940         final Object[] values = new Object[bundle.size()];
941         int i = 0;
942 
943         for (final String key : bundle.keySet()) {
944             final Object value = bundle.get(key);
945             keys[i] = key;
946 
947             if (value instanceof Bundle || value == null) {
948                 values[i] = fromBundle((Bundle) value);
949             } else if (value instanceof Parcelable[]) {
950                 final Parcelable[] array = (Parcelable[]) value;
951                 final GeckoBundle[] out = new GeckoBundle[array.length];
952                 for (int j = 0; j < array.length; j++) {
953                     out[j] = fromBundle((Bundle) array[j]);
954                 }
955                 values[i] = out;
956             } else if (value instanceof Boolean || value instanceof Integer ||
957                     value instanceof Double || value instanceof String ||
958                     value instanceof boolean[] || value instanceof int[] ||
959                     value instanceof double[] || value instanceof String[]) {
960                 values[i] = value;
961             } else if (value instanceof Byte || value instanceof Short) {
962                 values[i] = ((Number) value).intValue();
963             } else if (value instanceof Float || value instanceof Long) {
964                 values[i] = ((Number) value).doubleValue();
965             } else if (value instanceof CharSequence || value instanceof Character) {
966                 values[i] = value.toString();
967             } else {
968                 throw new UnsupportedOperationException();
969             }
970 
971             i++;
972         }
973         return new GeckoBundle(keys, values);
974     }
975 
fromJSONValue(final Object value)976     private static Object fromJSONValue(final Object value) throws JSONException {
977         if (value == null || value == JSONObject.NULL) {
978             return null;
979         } else if (value instanceof JSONObject) {
980             return fromJSONObject((JSONObject) value);
981         }
982         if (value instanceof JSONArray) {
983             final JSONArray array = (JSONArray) value;
984             final int len = array.length();
985             if (len == 0) {
986                 return EMPTY_BOOLEAN_ARRAY;
987             }
988             Object out = null;
989             for (int i = 0; i < len; i++) {
990                 final Object element = fromJSONValue(array.opt(i));
991                 if (element == null) {
992                     continue;
993                 }
994                 if (out == null) {
995                     Class<?> type = element.getClass();
996                     if (type == Boolean.class) {
997                         type = boolean.class;
998                     } else if (type == Integer.class) {
999                         type = int.class;
1000                     } else if (type == Double.class) {
1001                         type = double.class;
1002                     }
1003                     out = Array.newInstance(type, len);
1004                 }
1005                 Array.set(out, i, element);
1006             }
1007             if (out == null) {
1008                 // Treat all-null arrays as String arrays.
1009                 return new String[len];
1010             }
1011             return out;
1012         }
1013         if (value instanceof Boolean || value instanceof Integer ||
1014                 value instanceof Double || value instanceof String) {
1015             return value;
1016         }
1017         if (value instanceof Byte || value instanceof Short) {
1018             return ((Number) value).intValue();
1019         }
1020         if (value instanceof Float || value instanceof Long) {
1021             return ((Number) value).doubleValue();
1022         }
1023         return value.toString();
1024     }
1025 
fromJSONObject(final JSONObject obj)1026     public static GeckoBundle fromJSONObject(final JSONObject obj) throws JSONException {
1027         if (obj == null || obj == JSONObject.NULL) {
1028             return null;
1029         }
1030 
1031         final String[] keys = new String[obj.length()];
1032         final Object[] values = new Object[obj.length()];
1033 
1034         final Iterator<String> iter = obj.keys();
1035         for (int i = 0; iter.hasNext(); i++) {
1036             final String key = iter.next();
1037             keys[i] = key;
1038             values[i] = fromJSONValue(obj.opt(key));
1039         }
1040         return new GeckoBundle(keys, values);
1041     }
1042 
1043     @Override // Parcelable
describeContents()1044     public int describeContents() {
1045         return 0;
1046     }
1047 
1048     @Override // Parcelable
writeToParcel(final Parcel dest, final int flags)1049     public void writeToParcel(final Parcel dest, final int flags) {
1050         final int len = mMap.size();
1051         dest.writeInt(len);
1052 
1053         for (int i = 0; i < len; i++) {
1054             dest.writeString(mMap.keyAt(i));
1055             dest.writeValue(mMap.valueAt(i));
1056         }
1057     }
1058 
1059     // AIDL code may call readFromParcel even though it's not part of Parcelable.
readFromParcel(final Parcel source)1060     public void readFromParcel(final Parcel source) {
1061         final ClassLoader loader = getClass().getClassLoader();
1062         final int len = source.readInt();
1063         mMap.clear();
1064         mMap.ensureCapacity(len);
1065 
1066         for (int i = 0; i < len; i++) {
1067             final String key = source.readString();
1068             Object val = source.readValue(loader);
1069 
1070             if (val instanceof Parcelable[]) {
1071                 final Parcelable[] array = (Parcelable[]) val;
1072                 val = Arrays.copyOf(array, array.length, GeckoBundle[].class);
1073             }
1074 
1075             mMap.put(key, val);
1076         }
1077     }
1078 
1079     public static final Parcelable.Creator<GeckoBundle> CREATOR =
1080             new Parcelable.Creator<GeckoBundle>() {
1081         @Override
1082         public GeckoBundle createFromParcel(final Parcel source) {
1083             final GeckoBundle bundle = new GeckoBundle(0);
1084             bundle.readFromParcel(source);
1085             return bundle;
1086         }
1087 
1088         @Override
1089         public GeckoBundle[] newArray(final int size) {
1090             return new GeckoBundle[size];
1091         }
1092     };
1093 }
1094