483 lines
17 KiB
Java
483 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2014 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package android.os;
|
|
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.util.ArrayMap;
|
|
import android.util.Slog;
|
|
import android.util.Xml;
|
|
import android.util.proto.ProtoOutputStream;
|
|
|
|
import com.android.internal.util.XmlUtils;
|
|
import com.android.modules.utils.TypedXmlPullParser;
|
|
import com.android.modules.utils.TypedXmlSerializer;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlSerializer;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.Serializable;
|
|
import java.util.ArrayList;
|
|
|
|
/**
|
|
* A mapping from String keys to values of various types. The set of types
|
|
* supported by this class is purposefully restricted to simple objects that can
|
|
* safely be persisted to and restored from disk.
|
|
*
|
|
* <p><b>Warning:</b> Note that {@link PersistableBundle} is a lazy container and as such it does
|
|
* NOT implement {@link #equals(Object)} or {@link #hashCode()}.
|
|
*
|
|
* @see Bundle
|
|
*/
|
|
@android.ravenwood.annotation.RavenwoodKeepWholeClass
|
|
public final class PersistableBundle extends BaseBundle implements Cloneable, Parcelable,
|
|
XmlUtils.WriteMapCallback {
|
|
private static final String TAG = "PersistableBundle";
|
|
|
|
private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
|
|
|
|
/** An unmodifiable {@code PersistableBundle} that is always {@link #isEmpty() empty}. */
|
|
public static final PersistableBundle EMPTY;
|
|
|
|
static {
|
|
EMPTY = new PersistableBundle();
|
|
EMPTY.mMap = ArrayMap.EMPTY;
|
|
}
|
|
|
|
/** @hide */
|
|
public static boolean isValidType(Object value) {
|
|
return (value instanceof Integer) || (value instanceof Long) ||
|
|
(value instanceof Double) || (value instanceof String) ||
|
|
(value instanceof int[]) || (value instanceof long[]) ||
|
|
(value instanceof double[]) || (value instanceof String[]) ||
|
|
(value instanceof PersistableBundle) || (value == null) ||
|
|
(value instanceof Boolean) || (value instanceof boolean[]);
|
|
}
|
|
|
|
/**
|
|
* Constructs a new, empty PersistableBundle.
|
|
*/
|
|
public PersistableBundle() {
|
|
super();
|
|
mFlags = FLAG_DEFUSABLE;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new, empty PersistableBundle sized to hold the given number of
|
|
* elements. The PersistableBundle will grow as needed.
|
|
*
|
|
* @param capacity the initial capacity of the PersistableBundle
|
|
*/
|
|
public PersistableBundle(int capacity) {
|
|
super(capacity);
|
|
mFlags = FLAG_DEFUSABLE;
|
|
}
|
|
|
|
/**
|
|
* Constructs a PersistableBundle containing a copy of the mappings from the given
|
|
* PersistableBundle. Does only a shallow copy of the original PersistableBundle -- see
|
|
* {@link #deepCopy()} if that is not what you want.
|
|
*
|
|
* @param b a PersistableBundle to be copied.
|
|
*
|
|
* @see #deepCopy()
|
|
*/
|
|
public PersistableBundle(PersistableBundle b) {
|
|
super(b);
|
|
mFlags = b.mFlags;
|
|
}
|
|
|
|
|
|
/**
|
|
* Constructs a PersistableBundle from a Bundle. Does only a shallow copy of the Bundle.
|
|
*
|
|
* <p><b>Warning:</b> This method will deserialize every item on the bundle, including custom
|
|
* types such as {@link Parcelable} and {@link Serializable}, so only use this when you trust
|
|
* the source. Specifically don't use this method on app-provided bundles.
|
|
*
|
|
* @param b a Bundle to be copied.
|
|
*
|
|
* @throws IllegalArgumentException if any element of {@code b} cannot be persisted.
|
|
*
|
|
* @hide
|
|
*/
|
|
public PersistableBundle(Bundle b) {
|
|
this(b, true);
|
|
}
|
|
|
|
private PersistableBundle(Bundle b, boolean throwException) {
|
|
this(b.getItemwiseMap(), throwException);
|
|
}
|
|
|
|
/**
|
|
* Constructs a PersistableBundle containing the mappings passed in.
|
|
*
|
|
* @param map a Map containing only those items that can be persisted.
|
|
* @throws IllegalArgumentException if any element of #map cannot be persisted.
|
|
*/
|
|
private PersistableBundle(ArrayMap<String, Object> map, boolean throwException) {
|
|
super();
|
|
mFlags = FLAG_DEFUSABLE;
|
|
|
|
// First stuff everything in.
|
|
putAll(map);
|
|
|
|
// Now verify each item throwing an exception if there is a violation.
|
|
final int N = mMap.size();
|
|
for (int i = N - 1; i >= 0; --i) {
|
|
Object value = mMap.valueAt(i);
|
|
if (value instanceof ArrayMap) {
|
|
// Fix up any Maps by replacing them with PersistableBundles.
|
|
mMap.setValueAt(i,
|
|
new PersistableBundle((ArrayMap<String, Object>) value, throwException));
|
|
} else if (value instanceof Bundle) {
|
|
mMap.setValueAt(i, new PersistableBundle((Bundle) value, throwException));
|
|
} else if (!isValidType(value)) {
|
|
final String errorMsg = "Bad value in PersistableBundle key="
|
|
+ mMap.keyAt(i) + " value=" + value;
|
|
if (throwException) {
|
|
throw new IllegalArgumentException(errorMsg);
|
|
} else {
|
|
Slog.wtfStack(TAG, errorMsg);
|
|
mMap.removeAt(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* package */ PersistableBundle(Parcel parcelledData, int length) {
|
|
super(parcelledData, length);
|
|
mFlags = FLAG_DEFUSABLE;
|
|
}
|
|
|
|
/**
|
|
* Constructs a {@link PersistableBundle} containing a copy of {@code from}.
|
|
*
|
|
* @param from The bundle to be copied.
|
|
* @param deep Whether is a deep or shallow copy.
|
|
*
|
|
* @hide
|
|
*/
|
|
PersistableBundle(PersistableBundle from, boolean deep) {
|
|
super(from, deep);
|
|
}
|
|
|
|
/**
|
|
* Make a PersistableBundle for a single key/value pair.
|
|
*
|
|
* @hide
|
|
*/
|
|
public static PersistableBundle forPair(String key, String value) {
|
|
PersistableBundle b = new PersistableBundle(1);
|
|
b.putString(key, value);
|
|
return b;
|
|
}
|
|
|
|
/**
|
|
* Clones the current PersistableBundle. The internal map is cloned, but the keys and
|
|
* values to which it refers are copied by reference.
|
|
*/
|
|
@Override
|
|
public Object clone() {
|
|
return new PersistableBundle(this);
|
|
}
|
|
|
|
/**
|
|
* Make a deep copy of the given bundle. Traverses into inner containers and copies
|
|
* them as well, so they are not shared across bundles. Will traverse in to
|
|
* {@link Bundle}, {@link PersistableBundle}, {@link ArrayList}, and all types of
|
|
* primitive arrays. Other types of objects (such as Parcelable or Serializable)
|
|
* are referenced as-is and not copied in any way.
|
|
*/
|
|
public PersistableBundle deepCopy() {
|
|
return new PersistableBundle(this, /* deep */ true);
|
|
}
|
|
|
|
/**
|
|
* Inserts a PersistableBundle value into the mapping of this Bundle, replacing
|
|
* any existing value for the given key. Either key or value may be null.
|
|
*
|
|
* @param key a String, or null
|
|
* @param value a Bundle object, or null
|
|
*/
|
|
public void putPersistableBundle(@Nullable String key, @Nullable PersistableBundle value) {
|
|
unparcel();
|
|
mMap.put(key, value);
|
|
}
|
|
|
|
/**
|
|
* Returns the value associated with the given key, or null if
|
|
* no mapping of the desired type exists for the given key or a null
|
|
* value is explicitly associated with the key.
|
|
*
|
|
* @param key a String, or null
|
|
* @return a Bundle value, or null
|
|
*/
|
|
@Nullable
|
|
public PersistableBundle getPersistableBundle(@Nullable String key) {
|
|
unparcel();
|
|
Object o = mMap.get(key);
|
|
if (o == null) {
|
|
return null;
|
|
}
|
|
try {
|
|
return (PersistableBundle) o;
|
|
} catch (ClassCastException e) {
|
|
typeWarning(key, o, "Bundle", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static final @android.annotation.NonNull Parcelable.Creator<PersistableBundle> CREATOR =
|
|
new Parcelable.Creator<PersistableBundle>() {
|
|
@Override
|
|
public PersistableBundle createFromParcel(Parcel in) {
|
|
return in.readPersistableBundle();
|
|
}
|
|
|
|
@Override
|
|
public PersistableBundle[] newArray(int size) {
|
|
return new PersistableBundle[size];
|
|
}
|
|
};
|
|
|
|
/** @hide */
|
|
@Override
|
|
public void writeUnknownObject(Object v, String name, TypedXmlSerializer out)
|
|
throws XmlPullParserException, IOException {
|
|
if (v instanceof PersistableBundle) {
|
|
out.startTag(null, TAG_PERSISTABLEMAP);
|
|
out.attribute(null, "name", name);
|
|
((PersistableBundle) v).saveToXml(out);
|
|
out.endTag(null, TAG_PERSISTABLEMAP);
|
|
} else {
|
|
throw new XmlPullParserException("Unknown Object o=" + v);
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
|
|
saveToXml(XmlUtils.makeTyped(out));
|
|
}
|
|
|
|
/** @hide */
|
|
public void saveToXml(TypedXmlSerializer out) throws IOException, XmlPullParserException {
|
|
unparcel();
|
|
// Explicitly drop invalid types an attacker may have added before persisting.
|
|
for (int i = mMap.size() - 1; i >= 0; --i) {
|
|
final Object value = mMap.valueAt(i);
|
|
if (!isValidType(value)) {
|
|
Slog.e(TAG, "Dropping bad data before persisting: "
|
|
+ mMap.keyAt(i) + "=" + value);
|
|
mMap.removeAt(i);
|
|
}
|
|
}
|
|
XmlUtils.writeMapXml(mMap, out, this);
|
|
}
|
|
|
|
/**
|
|
* Checks whether all keys and values are within the given character limit.
|
|
* Note: Maximum character limit of String that can be saved to XML as part of bundle is 65535.
|
|
* Otherwise IOException is thrown.
|
|
* @param limit length of String keys and values in the PersistableBundle, including nested
|
|
* PersistableBundles to check against.
|
|
*
|
|
* @hide
|
|
*/
|
|
public boolean isBundleContentsWithinLengthLimit(int limit) {
|
|
unparcel();
|
|
if (mMap == null) {
|
|
return true;
|
|
}
|
|
for (int i = 0; i < mMap.size(); i++) {
|
|
if (mMap.keyAt(i) != null && mMap.keyAt(i).length() > limit) {
|
|
return false;
|
|
}
|
|
final Object value = mMap.valueAt(i);
|
|
if (value instanceof String && ((String) value).length() > limit) {
|
|
return false;
|
|
} else if (value instanceof String[]) {
|
|
String[] stringArray = (String[]) value;
|
|
for (int j = 0; j < stringArray.length; j++) {
|
|
if (stringArray[j] != null
|
|
&& stringArray[j].length() > limit) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (value instanceof PersistableBundle
|
|
&& !((PersistableBundle) value).isBundleContentsWithinLengthLimit(limit)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** @hide */
|
|
static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
|
|
@Override
|
|
public Object readThisUnknownObjectXml(TypedXmlPullParser in, String tag)
|
|
throws XmlPullParserException, IOException {
|
|
if (TAG_PERSISTABLEMAP.equals(tag)) {
|
|
return restoreFromXml(in);
|
|
}
|
|
throw new XmlPullParserException("Unknown tag=" + tag);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report the nature of this Parcelable's contents
|
|
*/
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Writes the PersistableBundle contents to a Parcel, typically in order for
|
|
* it to be passed through an IBinder connection.
|
|
* @param parcel The parcel to copy this bundle to.
|
|
*/
|
|
@Override
|
|
public void writeToParcel(Parcel parcel, int flags) {
|
|
final boolean oldAllowFds = parcel.pushAllowFds(false);
|
|
try {
|
|
writeToParcelInner(parcel, flags);
|
|
} finally {
|
|
parcel.restoreAllowFds(oldAllowFds);
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
|
|
XmlPullParserException {
|
|
return restoreFromXml(XmlUtils.makeTyped(in));
|
|
}
|
|
|
|
/** @hide */
|
|
public static PersistableBundle restoreFromXml(TypedXmlPullParser in) throws IOException,
|
|
XmlPullParserException {
|
|
final int outerDepth = in.getDepth();
|
|
final String startTag = in.getName();
|
|
final String[] tagName = new String[1];
|
|
int event;
|
|
while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
|
|
(event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
|
|
if (event == XmlPullParser.START_TAG) {
|
|
// Don't throw an exception when restoring from XML since an attacker could try to
|
|
// input invalid data in the persisted file.
|
|
return new PersistableBundle((ArrayMap<String, Object>)
|
|
XmlUtils.readThisArrayMapXml(in, startTag, tagName,
|
|
new MyReadMapCallback()),
|
|
/* throwException */ false);
|
|
}
|
|
}
|
|
return new PersistableBundle(); // An empty mutable PersistableBundle
|
|
}
|
|
|
|
/**
|
|
* Returns a string representation of the {@link PersistableBundle} that may be suitable for
|
|
* debugging. It won't print the internal map if its content hasn't been unparcelled.
|
|
*/
|
|
@Override
|
|
public synchronized String toString() {
|
|
if (mParcelledData != null) {
|
|
if (isEmptyParcel()) {
|
|
return "PersistableBundle[EMPTY_PARCEL]";
|
|
} else {
|
|
return "PersistableBundle[mParcelledData.dataSize=" +
|
|
mParcelledData.dataSize() + "]";
|
|
}
|
|
}
|
|
return "PersistableBundle[" + mMap.toString() + "]";
|
|
}
|
|
|
|
/** @hide */
|
|
synchronized public String toShortString() {
|
|
if (mParcelledData != null) {
|
|
if (isEmptyParcel()) {
|
|
return "EMPTY_PARCEL";
|
|
} else {
|
|
return "mParcelledData.dataSize=" + mParcelledData.dataSize();
|
|
}
|
|
}
|
|
return mMap.toString();
|
|
}
|
|
|
|
/** @hide */
|
|
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
|
|
final long token = proto.start(fieldId);
|
|
|
|
if (mParcelledData != null) {
|
|
if (isEmptyParcel()) {
|
|
proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, 0);
|
|
} else {
|
|
proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, mParcelledData.dataSize());
|
|
}
|
|
} else {
|
|
proto.write(PersistableBundleProto.MAP_DATA, mMap.toString());
|
|
}
|
|
|
|
proto.end(token);
|
|
}
|
|
|
|
/**
|
|
* Writes the content of the {@link PersistableBundle} to a {@link OutputStream}.
|
|
*
|
|
* <p>The content can be read by a {@link #readFromStream}.
|
|
*
|
|
* @see #readFromStream
|
|
*/
|
|
public void writeToStream(@NonNull OutputStream outputStream) throws IOException {
|
|
TypedXmlSerializer serializer = Xml.newFastSerializer();
|
|
serializer.setOutput(outputStream, UTF_8.name());
|
|
serializer.startTag(null, "bundle");
|
|
try {
|
|
saveToXml(serializer);
|
|
} catch (XmlPullParserException e) {
|
|
throw new IOException(e);
|
|
}
|
|
serializer.endTag(null, "bundle");
|
|
serializer.flush();
|
|
}
|
|
|
|
/**
|
|
* Reads a {@link PersistableBundle} from an {@link InputStream}.
|
|
*
|
|
* <p>The stream must be generated by {@link #writeToStream}.
|
|
*
|
|
* @see #writeToStream
|
|
*/
|
|
@NonNull
|
|
public static PersistableBundle readFromStream(@NonNull InputStream inputStream)
|
|
throws IOException {
|
|
try {
|
|
TypedXmlPullParser parser = Xml.newFastPullParser();
|
|
parser.setInput(inputStream, UTF_8.name());
|
|
parser.next();
|
|
return PersistableBundle.restoreFromXml(parser);
|
|
} catch (XmlPullParserException e) {
|
|
throw new IOException(e);
|
|
}
|
|
}
|
|
}
|