1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.os;
18
19import android.annotation.Nullable;
20import android.util.ArrayMap;
21
22import com.android.internal.util.XmlUtils;
23
24import org.xmlpull.v1.XmlPullParser;
25import org.xmlpull.v1.XmlPullParserException;
26import org.xmlpull.v1.XmlSerializer;
27
28import java.io.IOException;
29
30/**
31 * A mapping from String keys to values of various types. The set of types
32 * supported by this class is purposefully restricted to simple objects that can
33 * safely be persisted to and restored from disk.
34 *
35 * @see Bundle
36 */
37public final class PersistableBundle extends BaseBundle implements Cloneable, Parcelable,
38        XmlUtils.WriteMapCallback {
39    private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
40    public static final PersistableBundle EMPTY;
41
42    static {
43        EMPTY = new PersistableBundle();
44        EMPTY.mMap = ArrayMap.EMPTY;
45    }
46
47    /** @hide */
48    public static boolean isValidType(Object value) {
49        return (value instanceof Integer) || (value instanceof Long) ||
50                (value instanceof Double) || (value instanceof String) ||
51                (value instanceof int[]) || (value instanceof long[]) ||
52                (value instanceof double[]) || (value instanceof String[]) ||
53                (value instanceof PersistableBundle) || (value == null) ||
54                (value instanceof Boolean) || (value instanceof boolean[]);
55    }
56
57    /**
58     * Constructs a new, empty PersistableBundle.
59     */
60    public PersistableBundle() {
61        super();
62        mFlags = FLAG_DEFUSABLE;
63    }
64
65    /**
66     * Constructs a new, empty PersistableBundle sized to hold the given number of
67     * elements. The PersistableBundle will grow as needed.
68     *
69     * @param capacity the initial capacity of the PersistableBundle
70     */
71    public PersistableBundle(int capacity) {
72        super(capacity);
73        mFlags = FLAG_DEFUSABLE;
74    }
75
76    /**
77     * Constructs a PersistableBundle containing a copy of the mappings from the given
78     * PersistableBundle.
79     *
80     * @param b a PersistableBundle to be copied.
81     */
82    public PersistableBundle(PersistableBundle b) {
83        super(b);
84        mFlags = b.mFlags;
85    }
86
87
88    /**
89     * Constructs a PersistableBundle from a Bundle.
90     *
91     * @param b a Bundle to be copied.
92     *
93     * @throws IllegalArgumentException if any element of {@code b} cannot be persisted.
94     *
95     * @hide
96     */
97    public PersistableBundle(Bundle b) {
98        this(b.getMap());
99    }
100
101    /**
102     * Constructs a PersistableBundle containing the mappings passed in.
103     *
104     * @param map a Map containing only those items that can be persisted.
105     * @throws IllegalArgumentException if any element of #map cannot be persisted.
106     */
107    private PersistableBundle(ArrayMap<String, Object> map) {
108        super();
109        mFlags = FLAG_DEFUSABLE;
110
111        // First stuff everything in.
112        putAll(map);
113
114        // Now verify each item throwing an exception if there is a violation.
115        final int N = mMap.size();
116        for (int i=0; i<N; i++) {
117            Object value = mMap.valueAt(i);
118            if (value instanceof ArrayMap) {
119                // Fix up any Maps by replacing them with PersistableBundles.
120                mMap.setValueAt(i, new PersistableBundle((ArrayMap<String, Object>) value));
121            } else if (value instanceof Bundle) {
122                mMap.setValueAt(i, new PersistableBundle(((Bundle) value)));
123            } else if (!isValidType(value)) {
124                throw new IllegalArgumentException("Bad value in PersistableBundle key="
125                        + mMap.keyAt(i) + " value=" + value);
126            }
127        }
128    }
129
130    /* package */ PersistableBundle(Parcel parcelledData, int length) {
131        super(parcelledData, length);
132        mFlags = FLAG_DEFUSABLE;
133    }
134
135    /**
136     * Make a PersistableBundle for a single key/value pair.
137     *
138     * @hide
139     */
140    public static PersistableBundle forPair(String key, String value) {
141        PersistableBundle b = new PersistableBundle(1);
142        b.putString(key, value);
143        return b;
144    }
145
146    /**
147     * Clones the current PersistableBundle. The internal map is cloned, but the keys and
148     * values to which it refers are copied by reference.
149     */
150    @Override
151    public Object clone() {
152        return new PersistableBundle(this);
153    }
154
155    /**
156     * Inserts a PersistableBundle value into the mapping of this Bundle, replacing
157     * any existing value for the given key.  Either key or value may be null.
158     *
159     * @param key a String, or null
160     * @param value a Bundle object, or null
161     */
162    public void putPersistableBundle(@Nullable String key, @Nullable PersistableBundle value) {
163        unparcel();
164        mMap.put(key, value);
165    }
166
167    /**
168     * Returns the value associated with the given key, or null if
169     * no mapping of the desired type exists for the given key or a null
170     * value is explicitly associated with the key.
171     *
172     * @param key a String, or null
173     * @return a Bundle value, or null
174     */
175    @Nullable
176    public PersistableBundle getPersistableBundle(@Nullable String key) {
177        unparcel();
178        Object o = mMap.get(key);
179        if (o == null) {
180            return null;
181        }
182        try {
183            return (PersistableBundle) o;
184        } catch (ClassCastException e) {
185            typeWarning(key, o, "Bundle", e);
186            return null;
187        }
188    }
189
190    public static final Parcelable.Creator<PersistableBundle> CREATOR =
191            new Parcelable.Creator<PersistableBundle>() {
192                @Override
193                public PersistableBundle createFromParcel(Parcel in) {
194                    return in.readPersistableBundle();
195                }
196
197                @Override
198                public PersistableBundle[] newArray(int size) {
199                    return new PersistableBundle[size];
200                }
201            };
202
203    /** @hide */
204    @Override
205    public void writeUnknownObject(Object v, String name, XmlSerializer out)
206            throws XmlPullParserException, IOException {
207        if (v instanceof PersistableBundle) {
208            out.startTag(null, TAG_PERSISTABLEMAP);
209            out.attribute(null, "name", name);
210            ((PersistableBundle) v).saveToXml(out);
211            out.endTag(null, TAG_PERSISTABLEMAP);
212        } else {
213            throw new XmlPullParserException("Unknown Object o=" + v);
214        }
215    }
216
217    /** @hide */
218    public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
219        unparcel();
220        XmlUtils.writeMapXml(mMap, out, this);
221    }
222
223    /** @hide */
224    static class MyReadMapCallback implements  XmlUtils.ReadMapCallback {
225        @Override
226        public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
227                throws XmlPullParserException, IOException {
228            if (TAG_PERSISTABLEMAP.equals(tag)) {
229                return restoreFromXml(in);
230            }
231            throw new XmlPullParserException("Unknown tag=" + tag);
232        }
233    }
234
235    /**
236     * Report the nature of this Parcelable's contents
237     */
238    @Override
239    public int describeContents() {
240        return 0;
241    }
242
243    /**
244     * Writes the PersistableBundle contents to a Parcel, typically in order for
245     * it to be passed through an IBinder connection.
246     * @param parcel The parcel to copy this bundle to.
247     */
248    @Override
249    public void writeToParcel(Parcel parcel, int flags) {
250        final boolean oldAllowFds = parcel.pushAllowFds(false);
251        try {
252            writeToParcelInner(parcel, flags);
253        } finally {
254            parcel.restoreAllowFds(oldAllowFds);
255        }
256    }
257
258    /** @hide */
259    public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
260            XmlPullParserException {
261        final int outerDepth = in.getDepth();
262        final String startTag = in.getName();
263        final String[] tagName = new String[1];
264        int event;
265        while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
266                (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
267            if (event == XmlPullParser.START_TAG) {
268                return new PersistableBundle((ArrayMap<String, Object>)
269                        XmlUtils.readThisArrayMapXml(in, startTag, tagName,
270                        new MyReadMapCallback()));
271            }
272        }
273        return EMPTY;
274    }
275
276    @Override
277    synchronized public String toString() {
278        if (mParcelledData != null) {
279            if (isEmptyParcel()) {
280                return "PersistableBundle[EMPTY_PARCEL]";
281            } else {
282                return "PersistableBundle[mParcelledData.dataSize=" +
283                        mParcelledData.dataSize() + "]";
284            }
285        }
286        return "PersistableBundle[" + mMap.toString() + "]";
287    }
288}
289