Support for nested bundles in setApplicationRestrictions

Added new restriction types - bundle and bundle-array.

Modified RestrictionsManager.getManifestRestrictions to support new
hierarchical restrictions.
Added RestrictionsManager.convertRestrictionsToBundle, which enables
programmatic conversion from a list of RestrictionEntries to a Bundle.

Modified read/write methods for application restrictions in UserManagerService.
Added unit tests.

Bug: 19540606
Change-Id: I32b264e04d5d177ea5b4c39a8ace5ee0ce907970
diff --git a/api/current.txt b/api/current.txt
index 5371ca8..8dd8f20 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8377,6 +8377,7 @@
     ctor public RestrictionEntry(java.lang.String, boolean);
     ctor public RestrictionEntry(java.lang.String, java.lang.String[]);
     ctor public RestrictionEntry(java.lang.String, int);
+    ctor public RestrictionEntry(java.lang.String, android.content.RestrictionEntry[], boolean);
     ctor public RestrictionEntry(android.os.Parcel);
     method public int describeContents();
     method public java.lang.String[] getAllSelectedStrings();
@@ -8385,6 +8386,7 @@
     method public java.lang.String getDescription();
     method public int getIntValue();
     method public java.lang.String getKey();
+    method public android.content.RestrictionEntry[] getRestrictions();
     method public boolean getSelectedState();
     method public java.lang.String getSelectedString();
     method public java.lang.String getTitle();
@@ -8396,6 +8398,7 @@
     method public void setChoiceValues(android.content.Context, int);
     method public void setDescription(java.lang.String);
     method public void setIntValue(int);
+    method public void setRestrictions(android.content.RestrictionEntry[]);
     method public void setSelectedState(boolean);
     method public void setSelectedString(java.lang.String);
     method public void setTitle(java.lang.String);
@@ -8403,6 +8406,8 @@
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.content.RestrictionEntry> CREATOR;
     field public static final int TYPE_BOOLEAN = 1; // 0x1
+    field public static final int TYPE_BUNDLE = 7; // 0x7
+    field public static final int TYPE_BUNDLE_ARRAY = 8; // 0x8
     field public static final int TYPE_CHOICE = 2; // 0x2
     field public static final int TYPE_INTEGER = 5; // 0x5
     field public static final int TYPE_MULTI_SELECT = 4; // 0x4
@@ -8411,6 +8416,7 @@
   }
 
   public class RestrictionsManager {
+    method public static android.os.Bundle convertRestrictionsToBundle(java.util.List<android.content.RestrictionEntry>);
     method public android.content.Intent createLocalApprovalIntent();
     method public android.os.Bundle getApplicationRestrictions();
     method public java.util.List<android.content.RestrictionEntry> getManifestRestrictions(java.lang.String);
diff --git a/api/system-current.txt b/api/system-current.txt
index 3b57333..b1b5cf3 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -8598,6 +8598,7 @@
     ctor public RestrictionEntry(java.lang.String, boolean);
     ctor public RestrictionEntry(java.lang.String, java.lang.String[]);
     ctor public RestrictionEntry(java.lang.String, int);
+    ctor public RestrictionEntry(java.lang.String, android.content.RestrictionEntry[], boolean);
     ctor public RestrictionEntry(android.os.Parcel);
     method public int describeContents();
     method public java.lang.String[] getAllSelectedStrings();
@@ -8606,6 +8607,7 @@
     method public java.lang.String getDescription();
     method public int getIntValue();
     method public java.lang.String getKey();
+    method public android.content.RestrictionEntry[] getRestrictions();
     method public boolean getSelectedState();
     method public java.lang.String getSelectedString();
     method public java.lang.String getTitle();
@@ -8617,6 +8619,7 @@
     method public void setChoiceValues(android.content.Context, int);
     method public void setDescription(java.lang.String);
     method public void setIntValue(int);
+    method public void setRestrictions(android.content.RestrictionEntry[]);
     method public void setSelectedState(boolean);
     method public void setSelectedString(java.lang.String);
     method public void setTitle(java.lang.String);
@@ -8624,6 +8627,8 @@
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.content.RestrictionEntry> CREATOR;
     field public static final int TYPE_BOOLEAN = 1; // 0x1
+    field public static final int TYPE_BUNDLE = 7; // 0x7
+    field public static final int TYPE_BUNDLE_ARRAY = 8; // 0x8
     field public static final int TYPE_CHOICE = 2; // 0x2
     field public static final int TYPE_INTEGER = 5; // 0x5
     field public static final int TYPE_MULTI_SELECT = 4; // 0x4
@@ -8632,6 +8637,7 @@
   }
 
   public class RestrictionsManager {
+    method public static android.os.Bundle convertRestrictionsToBundle(java.util.List<android.content.RestrictionEntry>);
     method public android.content.Intent createLocalApprovalIntent();
     method public android.os.Bundle getApplicationRestrictions();
     method public java.util.List<android.content.RestrictionEntry> getManifestRestrictions(java.lang.String);
diff --git a/core/java/android/content/RestrictionEntry.java b/core/java/android/content/RestrictionEntry.java
index 6d79626..342ee38 100644
--- a/core/java/android/content/RestrictionEntry.java
+++ b/core/java/android/content/RestrictionEntry.java
@@ -20,6 +20,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Arrays;
+import java.util.Objects;
+
 /**
  * Applications can expose restrictions for a restricted user on a
  * multiuser device. The administrator can configure these restrictions that will then be
@@ -33,19 +36,19 @@
 public class RestrictionEntry implements Parcelable {
 
     /**
-     * A type of restriction. Use this type for information that needs to be transferred across
-     * but shouldn't be presented to the user in the UI. Stores a single String value.
+     * Hidden restriction type. Use this type for information that needs to be transferred
+     * across but shouldn't be presented to the user in the UI. Stores a single String value.
      */
     public static final int TYPE_NULL         = 0;
 
     /**
-     * A type of restriction. Use this for storing a boolean value, typically presented as
+     * Restriction of type "bool". Use this for storing a boolean value, typically presented as
      * a checkbox in the UI.
      */
     public static final int TYPE_BOOLEAN      = 1;
 
     /**
-     * A type of restriction. Use this for storing a string value, typically presented as
+     * Restriction of type "choice". Use this for storing a string value, typically presented as
      * a single-select list. Call {@link #setChoiceEntries(String[])} and
      * {@link #setChoiceValues(String[])} to set the localized list entries to present to the user
      * and the corresponding values, respectively.
@@ -53,7 +56,7 @@
     public static final int TYPE_CHOICE       = 2;
 
     /**
-     * A type of restriction. Use this for storing a string value, typically presented as
+     * Internal restriction type. Use this for storing a string value, typically presented as
      * a single-select list. Call {@link #setChoiceEntries(String[])} and
      * {@link #setChoiceValues(String[])} to set the localized list entries to present to the user
      * and the corresponding values, respectively.
@@ -64,8 +67,8 @@
     public static final int TYPE_CHOICE_LEVEL = 3;
 
     /**
-     * A type of restriction. Use this for presenting a multi-select list where more than one
-     * entry can be selected, such as for choosing specific titles to white-list.
+     * Restriction of type "multi-select". Use this for presenting a multi-select list where more
+     * than one entry can be selected, such as for choosing specific titles to white-list.
      * Call {@link #setChoiceEntries(String[])} and
      * {@link #setChoiceValues(String[])} to set the localized list entries to present to the user
      * and the corresponding values, respectively.
@@ -75,18 +78,30 @@
     public static final int TYPE_MULTI_SELECT = 4;
 
     /**
-     * A type of restriction. Use this for storing an integer value. The range of values
+     * Restriction of type "integer". Use this for storing an integer value. The range of values
      * is from {@link Integer#MIN_VALUE} to {@link Integer#MAX_VALUE}.
      */
     public static final int TYPE_INTEGER = 5;
 
     /**
-     * A type of restriction. Use this for storing a string value.
+     * Restriction of type "string". Use this for storing a string value.
      * @see #setSelectedString
      * @see #getSelectedString
      */
     public static final int TYPE_STRING = 6;
 
+    /**
+     * Restriction of type "bundle". Use this for storing {@link android.os.Bundle bundles} of
+     * restrictions
+     */
+    public static final int TYPE_BUNDLE = 7;
+
+    /**
+     * Restriction of type "bundle_array". Use this for storing arrays of
+     * {@link android.os.Bundle bundles} of restrictions
+     */
+    public static final int TYPE_BUNDLE_ARRAY = 8;
+
     /** The type of restriction. */
     private int mType;
 
@@ -100,13 +115,13 @@
     private String mDescription;
 
     /** The user-visible set of choices used for single-select and multi-select lists. */
-    private String [] mChoiceEntries;
+    private String[] mChoiceEntries;
 
     /** The values corresponding to the user-visible choices. The value(s) of this entry will
      * one or more of these, returned by {@link #getAllSelectedStrings()} and
      * {@link #getSelectedString()}.
      */
-    private String [] mChoiceValues;
+    private String[] mChoiceValues;
 
     /* The chosen value, whose content depends on the type of the restriction. */
     private String mCurrentValue;
@@ -115,6 +130,12 @@
     private String[] mCurrentValues;
 
     /**
+     * List of nested restrictions. Used by {@link #TYPE_BUNDLE bundle} and
+     * {@link #TYPE_BUNDLE_ARRAY bundle_array} restrictions.
+     */
+    private RestrictionEntry[] mRestrictions;
+
+    /**
      * Constructor for specifying the type and key, with no initial value;
      *
      * @param type the restriction type.
@@ -170,6 +191,35 @@
     }
 
     /**
+     * Constructor for {@link #TYPE_BUNDLE}/{@link #TYPE_BUNDLE_ARRAY} type.
+     * @param key the unique key for this restriction
+     * @param restrictionEntries array of nested restriction entries. If the entry, being created
+     * represents a {@link #TYPE_BUNDLE_ARRAY bundle-array}, {@code restrictionEntries} array may
+     * only contain elements of type {@link #TYPE_BUNDLE bundle}.
+     * @param isBundleArray true if this restriction represents
+     * {@link #TYPE_BUNDLE_ARRAY bundle-array} type, otherwise the type will be set to
+     * {@link #TYPE_BUNDLE bundle}.
+     */
+    public RestrictionEntry(String key, RestrictionEntry[] restrictionEntries,
+            boolean isBundleArray) {
+        mKey = key;
+        if (isBundleArray) {
+            mType = TYPE_BUNDLE_ARRAY;
+            if (restrictionEntries != null) {
+                for (RestrictionEntry restriction : restrictionEntries) {
+                    if (restriction.getType() != TYPE_BUNDLE) {
+                        throw new IllegalArgumentException("bundle_array restriction can only have "
+                                + "nested restriction entries of type bundle");
+                    }
+                }
+            }
+        } else {
+            mType = TYPE_BUNDLE;
+        }
+        setRestrictions(restrictionEntries);
+    }
+
+    /**
      * Sets the type for this restriction.
      * @param type the type for this restriction.
      */
@@ -283,6 +333,22 @@
     }
 
     /**
+     * Returns array of possible restriction entries that this entry may contain.
+     */
+    public RestrictionEntry[] getRestrictions() {
+        return mRestrictions;
+    }
+
+   /**
+    * Sets an array of possible restriction entries, that this entry may contain.
+    * <p>This method is only relevant for types {@link #TYPE_BUNDLE} and
+    * {@link #TYPE_BUNDLE_ARRAY}
+    */
+    public void setRestrictions(RestrictionEntry[] restrictions) {
+        mRestrictions = restrictions;
+    }
+
+    /**
      * Returns the list of possible string values set earlier.
      * @return the list of possible values.
      */
@@ -362,27 +428,30 @@
         this.mTitle = title;
     }
 
-    private boolean equalArrays(String[] one, String[] other) {
-        if (one.length != other.length) return false;
-        for (int i = 0; i < one.length; i++) {
-            if (!one[i].equals(other[i])) return false;
-        }
-        return true;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (o == this) return true;
         if (!(o instanceof RestrictionEntry)) return false;
         final RestrictionEntry other = (RestrictionEntry) o;
-        // Make sure that either currentValue matches or currentValues matches.
-        return mType == other.mType && mKey.equals(other.mKey)
-                &&
-                ((mCurrentValues == null && other.mCurrentValues == null
-                  && mCurrentValue != null && mCurrentValue.equals(other.mCurrentValue))
-                 ||
-                 (mCurrentValue == null && other.mCurrentValue == null
-                  && mCurrentValues != null && equalArrays(mCurrentValues, other.mCurrentValues)));
+        if (mType != other.mType || mKey.equals(other.mKey)) {
+            return false;
+        }
+        if (mCurrentValues == null && other.mCurrentValues == null
+                && mRestrictions == null && other.mRestrictions == null
+                && Objects.equals(mCurrentValue, other.mCurrentValue)) {
+            return true;
+        }
+        if (mCurrentValue == null && other.mCurrentValue == null
+                && mRestrictions == null && other.mRestrictions == null
+                && Arrays.equals(mCurrentValues, other.mCurrentValues)) {
+            return true;
+        }
+        if (mCurrentValue == null && other.mCurrentValue == null
+                && mCurrentValue == null && other.mCurrentValue == null
+                && Arrays.equals(mRestrictions, other.mRestrictions)) {
+            return true;
+        }
+        return false;
     }
 
     @Override
@@ -397,28 +466,28 @@
                     result = 31 * result + value.hashCode();
                 }
             }
+        } else if (mRestrictions != null) {
+            result = 31 * result + Arrays.hashCode(mRestrictions);
         }
         return result;
     }
 
-    private String[] readArray(Parcel in) {
-        int count = in.readInt();
-        String[] values = new String[count];
-        for (int i = 0; i < count; i++) {
-            values[i] = in.readString();
-        }
-        return values;
-    }
-
     public RestrictionEntry(Parcel in) {
         mType = in.readInt();
         mKey = in.readString();
         mTitle = in.readString();
         mDescription = in.readString();
-        mChoiceEntries = readArray(in);
-        mChoiceValues = readArray(in);
+        mChoiceEntries = in.readStringArray();
+        mChoiceValues = in.readStringArray();
         mCurrentValue = in.readString();
-        mCurrentValues = readArray(in);
+        mCurrentValues = in.readStringArray();
+        Parcelable[] parcelables = in.readParcelableArray(null);
+        if (parcelables != null) {
+            mRestrictions = new RestrictionEntry[parcelables.length];
+            for (int i = 0; i < parcelables.length; i++) {
+                mRestrictions[i] = (RestrictionEntry) parcelables[i];
+            }
+        }
     }
 
     @Override
@@ -426,27 +495,17 @@
         return 0;
     }
 
-    private void writeArray(Parcel dest, String[] values) {
-        if (values == null) {
-            dest.writeInt(0);
-        } else {
-            dest.writeInt(values.length);
-            for (int i = 0; i < values.length; i++) {
-                dest.writeString(values[i]);
-            }
-        }
-    }
-
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(mType);
         dest.writeString(mKey);
         dest.writeString(mTitle);
         dest.writeString(mDescription);
-        writeArray(dest, mChoiceEntries);
-        writeArray(dest, mChoiceValues);
+        dest.writeStringArray(mChoiceEntries);
+        dest.writeStringArray(mChoiceValues);
         dest.writeString(mCurrentValue);
-        writeArray(dest, mCurrentValues);
+        dest.writeStringArray(mCurrentValues);
+        dest.writeParcelableArray(mRestrictions, 0);
     }
 
     public static final Creator<RestrictionEntry> CREATOR = new Creator<RestrictionEntry>() {
@@ -461,6 +520,16 @@
 
     @Override
     public String toString() {
-        return "RestrictionsEntry {type=" + mType + ", key=" + mKey + ", value=" + mCurrentValue + "}";
+        return "RestrictionEntry{" +
+                "mType=" + mType +
+                ", mKey='" + mKey + '\'' +
+                ", mTitle='" + mTitle + '\'' +
+                ", mDescription='" + mDescription + '\'' +
+                ", mChoiceEntries=" + Arrays.toString(mChoiceEntries) +
+                ", mChoiceValues=" + Arrays.toString(mChoiceValues) +
+                ", mCurrentValue='" + mCurrentValue + '\'' +
+                ", mCurrentValues=" + Arrays.toString(mCurrentValues) +
+                ", mRestrictions=" + Arrays.toString(mRestrictions) +
+                '}';
     }
 }
diff --git a/core/java/android/content/RestrictionsManager.java b/core/java/android/content/RestrictionsManager.java
index 21a6a0d..1fac06e 100644
--- a/core/java/android/content/RestrictionsManager.java
+++ b/core/java/android/content/RestrictionsManager.java
@@ -32,12 +32,14 @@
 import android.util.Xml;
 
 import com.android.internal.R;
+import com.android.internal.util.XmlUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -71,12 +73,15 @@
  *         android:key="string"
  *         android:title="string resource"
  *         android:restrictionType=["bool" | "string" | "integer"
- *                                         | "choice" | "multi-select" | "hidden"]
+ *                                         | "choice" | "multi-select" | "hidden"
+ *                                         | "bundle" | "bundle_array"]
  *         android:description="string resource"
  *         android:entries="string-array resource"
  *         android:entryValues="string-array resource"
- *         android:defaultValue="reference"
- *         /&gt;
+ *         android:defaultValue="reference" &gt;
+ *             &lt;restriction ... /&gt;
+ *             ...
+ *     &lt;/restriction&gt;
  *     &lt;restriction ... /&gt;
  *     ...
  * &lt;/restrictions&gt;
@@ -97,6 +102,9 @@
  * administrator controlling the values, if the title is not sufficient.</li>
  * </ul>
  * <p>
+ * Only restrictions of type {@code bundle} and {@code bundle_array} can have one or multiple nested
+ * restriction elements.
+ * <p>
  * In your manifest's <code>application</code> section, add the meta-data tag to point to
  * the restrictions XML file as shown below:
  * <pre>
@@ -537,9 +545,7 @@
 
         XmlResourceParser xml =
                 appInfo.loadXmlMetaData(mContext.getPackageManager(), META_DATA_APP_RESTRICTIONS);
-        List<RestrictionEntry> restrictions = loadManifestRestrictions(packageName, xml);
-
-        return restrictions;
+        return loadManifestRestrictions(packageName, xml);
     }
 
     private List<RestrictionEntry> loadManifestRestrictions(String packageName,
@@ -550,23 +556,16 @@
         } catch (NameNotFoundException nnfe) {
             return null;
         }
-        ArrayList<RestrictionEntry> restrictions = new ArrayList<RestrictionEntry>();
+        ArrayList<RestrictionEntry> restrictions = new ArrayList<>();
         RestrictionEntry restriction;
 
         try {
             int tagType = xml.next();
             while (tagType != XmlPullParser.END_DOCUMENT) {
                 if (tagType == XmlPullParser.START_TAG) {
-                    if (xml.getName().equals(TAG_RESTRICTION)) {
-                        AttributeSet attrSet = Xml.asAttributeSet(xml);
-                        if (attrSet != null) {
-                            TypedArray a = appContext.obtainStyledAttributes(attrSet,
-                                    com.android.internal.R.styleable.RestrictionEntry);
-                            restriction = loadRestriction(appContext, a);
-                            if (restriction != null) {
-                                restrictions.add(restriction);
-                            }
-                        }
+                    restriction = loadRestrictionElement(appContext, xml);
+                    if (restriction != null) {
+                        restrictions.add(restriction);
                     }
                 }
                 tagType = xml.next();
@@ -582,7 +581,21 @@
         return restrictions;
     }
 
-    private RestrictionEntry loadRestriction(Context appContext, TypedArray a) {
+    private RestrictionEntry loadRestrictionElement(Context appContext, XmlResourceParser xml)
+            throws IOException, XmlPullParserException {
+        if (xml.getName().equals(TAG_RESTRICTION)) {
+            AttributeSet attrSet = Xml.asAttributeSet(xml);
+            if (attrSet != null) {
+                TypedArray a = appContext.obtainStyledAttributes(attrSet,
+                        com.android.internal.R.styleable.RestrictionEntry);
+                return loadRestriction(appContext, a, xml);
+            }
+        }
+        return null;
+    }
+
+    private RestrictionEntry loadRestriction(Context appContext, TypedArray a, XmlResourceParser xml)
+            throws IOException, XmlPullParserException {
         String key = a.getString(R.styleable.RestrictionEntry_key);
         int restrictionType = a.getInt(
                 R.styleable.RestrictionEntry_restrictionType, -1);
@@ -633,9 +646,90 @@
                 restriction.setSelectedState(
                         a.getBoolean(R.styleable.RestrictionEntry_defaultValue, false));
                 break;
+            case RestrictionEntry.TYPE_BUNDLE:
+            case RestrictionEntry.TYPE_BUNDLE_ARRAY:
+                final int outerDepth = xml.getDepth();
+                List<RestrictionEntry> restrictionEntries = new ArrayList<>();
+                while (XmlUtils.nextElementWithin(xml, outerDepth)) {
+                    RestrictionEntry childEntry = loadRestrictionElement(appContext, xml);
+                    if (childEntry == null) {
+                        Log.w(TAG, "Child entry cannot be loaded for bundle restriction " + key);
+                    } else {
+                        restrictionEntries.add(childEntry);
+                        if (restrictionType == RestrictionEntry.TYPE_BUNDLE_ARRAY
+                                && childEntry.getType() != RestrictionEntry.TYPE_BUNDLE) {
+                            Log.w(TAG, "bundle_array " + key
+                                    + " can only contain entries of type bundle");
+                        }
+                    }
+                }
+                restriction.setRestrictions(restrictionEntries.toArray(new RestrictionEntry[
+                        restrictionEntries.size()]));
+                break;
             default:
                 Log.w(TAG, "Unknown restriction type " + restrictionType);
         }
         return restriction;
     }
+
+    /**
+     * Converts a list of restrictions to the corresponding bundle, using the following mapping:
+     * <table>
+     *     <tr><th>RestrictionEntry</th><th>Bundle</th></tr>
+     *     <tr><td>{@link RestrictionEntry#TYPE_BOOLEAN}</td><td>{@link Bundle#putBoolean}</td></tr>
+     *     <tr><td>{@link RestrictionEntry#TYPE_CHOICE}, {@link RestrictionEntry#TYPE_CHOICE}</td>
+     *     <td>{@link Bundle#putStringArray}</td></tr>
+     *     <tr><td>{@link RestrictionEntry#TYPE_INTEGER}</td><td>{@link Bundle#putInt}</td></tr>
+     *     <tr><td>{@link RestrictionEntry#TYPE_STRING}</td><td>{@link Bundle#putString}</td></tr>
+     *     <tr><td>{@link RestrictionEntry#TYPE_BUNDLE}</td><td>{@link Bundle#putBundle}</td></tr>
+     *     <tr><td>{@link RestrictionEntry#TYPE_BUNDLE_ARRAY}</td>
+     *     <td>{@link Bundle#putParcelableArray}</td></tr>
+     * </table>
+     * @param entries list of restrictions
+     */
+    public static Bundle convertRestrictionsToBundle(List<RestrictionEntry> entries) {
+        final Bundle bundle = new Bundle();
+        for (RestrictionEntry entry : entries) {
+            addRestrictionToBundle(bundle, entry);
+        }
+        return bundle;
+    }
+
+    private static Bundle addRestrictionToBundle(Bundle bundle, RestrictionEntry entry) {
+        switch (entry.getType()) {
+            case RestrictionEntry.TYPE_BOOLEAN:
+                bundle.putBoolean(entry.getKey(), entry.getSelectedState());
+                break;
+            case RestrictionEntry.TYPE_CHOICE:
+            case RestrictionEntry.TYPE_CHOICE_LEVEL:
+            case RestrictionEntry.TYPE_MULTI_SELECT:
+                bundle.putStringArray(entry.getKey(), entry.getAllSelectedStrings());
+                break;
+            case RestrictionEntry.TYPE_INTEGER:
+                bundle.putInt(entry.getKey(), entry.getIntValue());
+                break;
+            case RestrictionEntry.TYPE_STRING:
+            case RestrictionEntry.TYPE_NULL:
+                bundle.putString(entry.getKey(), entry.getSelectedString());
+                break;
+            case RestrictionEntry.TYPE_BUNDLE:
+                RestrictionEntry[] restrictions = entry.getRestrictions();
+                Bundle childBundle = convertRestrictionsToBundle(Arrays.asList(restrictions));
+                bundle.putBundle(entry.getKey(), childBundle);
+                break;
+            case RestrictionEntry.TYPE_BUNDLE_ARRAY:
+                restrictions = entry.getRestrictions();
+                Bundle[] bundleArray = new Bundle[restrictions.length];
+                for (int i = 0; i < restrictions.length; i++) {
+                    bundleArray[i] = addRestrictionToBundle(new Bundle(), restrictions[i]);
+                }
+                bundle.putParcelableArray(entry.getKey(), bundleArray);
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unsupported restrictionEntry type: " + entry.getType());
+        }
+        return bundle;
+    }
+
 }
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index dcb4b9e..b6d32b2 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7759,6 +7759,8 @@
             <enum name="multi-select" value="4" />
             <enum name="integer" value="5" />
             <enum name="string" value="6" />
+            <enum name="bundle" value="7" />
+            <enum name="bundle_array" value="8" />
         </attr>
         <attr name="title" />
         <attr name="description" />
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index bfaea8f..b07d338 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -113,6 +113,9 @@
     <application android:theme="@style/Theme">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
+        <meta-data
+            android:name="android.content.APP_RESTRICTIONS"
+            android:resource="@xml/app_restrictions" />
         <activity android:name="android.view.ViewAttachTestActivity" android:label="View Attach Test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/core/tests/coretests/res/values/strings.xml b/core/tests/coretests/res/values/strings.xml
index ce0d9a2..04b0478 100644
--- a/core/tests/coretests/res/values/strings.xml
+++ b/core/tests/coretests/res/values/strings.xml
@@ -135,4 +135,8 @@
     <string name="first">Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.</string>
     <string name="actor">Abe Lincoln</string>
     <string name="caption">Lincoln adressing the crowd at Gettysburgh</string>
+
+    <!-- RestrictionsManagerTest -->
+    <string name="restrictionManager_title">Title</string>
+    <string name="restrictionManager_desc">Description</string>
 </resources>
diff --git a/core/tests/coretests/res/xml/app_restrictions.xml b/core/tests/coretests/res/xml/app_restrictions.xml
new file mode 100644
index 0000000..c84cabcc
--- /dev/null
+++ b/core/tests/coretests/res/xml/app_restrictions.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
+    <restriction android:key="hidden_key"
+                 android:restrictionType="hidden"/>
+    <restriction
+            android:defaultValue="true"
+            android:description="@string/restrictionManager_desc"
+            android:key="bool_key"
+            android:restrictionType="bool"
+            android:title="@string/restrictionManager_title"/>
+    <restriction
+            android:defaultValue="test"
+            android:key="string_key"
+            android:restrictionType="string"/>
+
+    <restriction android:key="int_key"
+                 android:restrictionType="integer"
+                 android:defaultValue="15"/>
+    <restriction android:key="bundle_key"
+                 android:restrictionType="bundle">
+        <restriction
+                android:key="bundle_string_key"
+                android:restrictionType="string"/>
+        <restriction
+                android:defaultValue="true"
+                android:key="bundle_bool_key"
+                android:restrictionType="bool"/>
+
+    </restriction>
+    <restriction android:key="bundle_array_key"
+                 android:restrictionType="bundle_array">
+        <restriction android:key="bundle_array_int"
+                     android:restrictionType="integer"/>
+        <restriction android:key="bundle_array_bundle_key"
+                     android:restrictionType="bundle">
+            <restriction android:key="bundle_array_bundle_int_key"
+                         android:restrictionType="integer"/>
+        </restriction>
+    </restriction>
+</restrictions>
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/content/RestrictionsManagerTest.java b/core/tests/coretests/src/android/content/RestrictionsManagerTest.java
new file mode 100644
index 0000000..8921924
--- /dev/null
+++ b/core/tests/coretests/src/android/content/RestrictionsManagerTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 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.content;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.test.AndroidTestCase;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class RestrictionsManagerTest extends AndroidTestCase {
+    private RestrictionsManager mRm;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mRm = (RestrictionsManager) mContext.getSystemService(Context.RESTRICTIONS_SERVICE);
+    }
+
+    public void testGetManifestRestrictions() {
+        String packageName = getContext().getPackageName();
+        List<RestrictionEntry> manifestRestrictions = mRm.getManifestRestrictions(packageName);
+        assertEquals(6, manifestRestrictions.size());
+        Set<String> verifiedKeys = new HashSet<>(Arrays.asList("bundle_key", "bundle_array_key",
+                "bundle_array_bundle_key"));
+        for (RestrictionEntry entry : manifestRestrictions) {
+            if ("bundle_key".equals(entry.getKey())) {
+                assertEquals("bundle_key entry should have 2 children entries",
+                        2, entry.getRestrictions().length);
+                verifiedKeys.remove(entry.getKey());
+            } else if ("bundle_array_key".equals(entry.getKey())) {
+                assertEquals("bundle_array_key should have 2 children entries",
+                        2, entry.getRestrictions().length);
+                assertNotNull(entry.getRestrictions());
+                for (RestrictionEntry childEntry : entry.getRestrictions()) {
+                    if ("bundle_array_bundle_key".equals(childEntry.getKey())) {
+                        assertNotNull(childEntry.getRestrictions());
+                        assertEquals("bundle_array_bundle_key should have 1 child entry",
+                                1, childEntry.getRestrictions().length);
+                        verifiedKeys.remove(childEntry.getKey());
+                    }
+                }
+                verifiedKeys.remove(entry.getKey());
+            }
+        }
+        assertTrue("Entries" + verifiedKeys + " were not found", verifiedKeys.isEmpty());
+    }
+
+    public void testConvertRestrictionsToBundle() {
+        String packageName = getContext().getPackageName();
+        List<RestrictionEntry> manifestRestrictions = mRm.getManifestRestrictions(packageName);
+        Bundle bundle = RestrictionsManager.convertRestrictionsToBundle(manifestRestrictions);
+        assertEquals(6, bundle.size());
+        Bundle childBundle = bundle.getBundle("bundle_key");
+        assertNotNull(childBundle);
+        assertEquals(2, childBundle.size());
+        Parcelable[] childBundleArray = bundle.getParcelableArray("bundle_array_key");
+        assertEquals(2, childBundleArray.length);
+    }
+
+}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8cc9d19..5e58cd9 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -36,6 +36,7 @@
 import android.os.IUserManager;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -49,9 +50,11 @@
 import android.util.TimeUtils;
 import android.util.Xml;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -71,6 +74,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import libcore.io.IoUtils;
+
 public class UserManagerService extends IUserManager.Stub {
 
     private static final String LOG_TAG = "UserManagerService";
@@ -107,6 +112,8 @@
     private static final String ATTR_TYPE_STRING = "s";
     private static final String ATTR_TYPE_BOOLEAN = "b";
     private static final String ATTR_TYPE_INTEGER = "i";
+    private static final String ATTR_TYPE_BUNDLE = "B";
+    private static final String ATTR_TYPE_BUNDLE_ARRAY = "BA";
 
     private static final String USER_INFO_DIR = "system" + File.separator + "users";
     private static final String USER_LIST_FILENAME = "userlist.xml";
@@ -1672,124 +1679,168 @@
 
     private Bundle readApplicationRestrictionsLocked(String packageName,
             int userId) {
+        AtomicFile restrictionsFile =
+                new AtomicFile(new File(Environment.getUserSystemDirectory(userId),
+                        packageToRestrictionsFileName(packageName)));
+        return readApplicationRestrictionsLocked(restrictionsFile);
+    }
+
+    @VisibleForTesting
+    static Bundle readApplicationRestrictionsLocked(AtomicFile restrictionsFile) {
         final Bundle restrictions = new Bundle();
-        final ArrayList<String> values = new ArrayList<String>();
+        final ArrayList<String> values = new ArrayList<>();
 
         FileInputStream fis = null;
         try {
-            AtomicFile restrictionsFile =
-                    new AtomicFile(new File(Environment.getUserSystemDirectory(userId),
-                            packageToRestrictionsFileName(packageName)));
             fis = restrictionsFile.openRead();
             XmlPullParser parser = Xml.newPullParser();
             parser.setInput(fis, null);
-            int type;
-            while ((type = parser.next()) != XmlPullParser.START_TAG
-                    && type != XmlPullParser.END_DOCUMENT) {
-                ;
-            }
-
-            if (type != XmlPullParser.START_TAG) {
+            XmlUtils.nextElement(parser);
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
                 Slog.e(LOG_TAG, "Unable to read restrictions file "
                         + restrictionsFile.getBaseFile());
                 return restrictions;
             }
-
-            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_ENTRY)) {
-                    String key = parser.getAttributeValue(null, ATTR_KEY);
-                    String valType = parser.getAttributeValue(null, ATTR_VALUE_TYPE);
-                    String multiple = parser.getAttributeValue(null, ATTR_MULTIPLE);
-                    if (multiple != null) {
-                        values.clear();
-                        int count = Integer.parseInt(multiple);
-                        while (count > 0 && (type = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                            if (type == XmlPullParser.START_TAG
-                                    && parser.getName().equals(TAG_VALUE)) {
-                                values.add(parser.nextText().trim());
-                                count--;
-                            }
-                        }
-                        String [] valueStrings = new String[values.size()];
-                        values.toArray(valueStrings);
-                        restrictions.putStringArray(key, valueStrings);
-                    } else {
-                        String value = parser.nextText().trim();
-                        if (ATTR_TYPE_BOOLEAN.equals(valType)) {
-                            restrictions.putBoolean(key, Boolean.parseBoolean(value));
-                        } else if (ATTR_TYPE_INTEGER.equals(valType)) {
-                            restrictions.putInt(key, Integer.parseInt(value));
-                        } else {
-                            restrictions.putString(key, value);
-                        }
-                    }
-                }
+            while (parser.next() != XmlPullParser.END_DOCUMENT) {
+                readEntry(restrictions, values, parser);
             }
-        } catch (IOException ioe) {
-        } catch (XmlPullParserException pe) {
+        } catch (IOException|XmlPullParserException e) {
+            Log.w(LOG_TAG, "Error parsing " + restrictionsFile.getBaseFile(), e);
         } finally {
-            if (fis != null) {
-                try {
-                    fis.close();
-                } catch (IOException e) {
-                }
-            }
+            IoUtils.closeQuietly(fis);
         }
         return restrictions;
     }
 
+    private static void readEntry(Bundle restrictions, ArrayList<String> values,
+            XmlPullParser parser) throws XmlPullParserException, IOException {
+        int type = parser.getEventType();
+        if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_ENTRY)) {
+            String key = parser.getAttributeValue(null, ATTR_KEY);
+            String valType = parser.getAttributeValue(null, ATTR_VALUE_TYPE);
+            String multiple = parser.getAttributeValue(null, ATTR_MULTIPLE);
+            if (multiple != null) {
+                values.clear();
+                int count = Integer.parseInt(multiple);
+                while (count > 0 && (type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+                    if (type == XmlPullParser.START_TAG
+                            && parser.getName().equals(TAG_VALUE)) {
+                        values.add(parser.nextText().trim());
+                        count--;
+                    }
+                }
+                String [] valueStrings = new String[values.size()];
+                values.toArray(valueStrings);
+                restrictions.putStringArray(key, valueStrings);
+            } else if (ATTR_TYPE_BUNDLE.equals(valType)) {
+                restrictions.putBundle(key, readBundleEntry(parser, values));
+            } else if (ATTR_TYPE_BUNDLE_ARRAY.equals(valType)) {
+                final int outerDepth = parser.getDepth();
+                ArrayList<Bundle> bundleList = new ArrayList<>();
+                while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                    Bundle childBundle = readBundleEntry(parser, values);
+                    bundleList.add(childBundle);
+                }
+                restrictions.putParcelableArray(key,
+                        bundleList.toArray(new Bundle[bundleList.size()]));
+            } else {
+                String value = parser.nextText().trim();
+                if (ATTR_TYPE_BOOLEAN.equals(valType)) {
+                    restrictions.putBoolean(key, Boolean.parseBoolean(value));
+                } else if (ATTR_TYPE_INTEGER.equals(valType)) {
+                    restrictions.putInt(key, Integer.parseInt(value));
+                } else {
+                    restrictions.putString(key, value);
+                }
+            }
+        }
+    }
+
+    private static Bundle readBundleEntry(XmlPullParser parser, ArrayList<String> values)
+            throws IOException, XmlPullParserException {
+        Bundle childBundle = new Bundle();
+        final int outerDepth = parser.getDepth();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            readEntry(childBundle, values, parser);
+        }
+        return childBundle;
+    }
+
     private void writeApplicationRestrictionsLocked(String packageName,
             Bundle restrictions, int userId) {
-        FileOutputStream fos = null;
         AtomicFile restrictionsFile = new AtomicFile(
                 new File(Environment.getUserSystemDirectory(userId),
                         packageToRestrictionsFileName(packageName)));
+        writeApplicationRestrictionsLocked(restrictions, restrictionsFile);
+    }
+
+    @VisibleForTesting
+    static void writeApplicationRestrictionsLocked(Bundle restrictions,
+            AtomicFile restrictionsFile) {
+        FileOutputStream fos = null;
         try {
             fos = restrictionsFile.startWrite();
             final BufferedOutputStream bos = new BufferedOutputStream(fos);
 
-            // XmlSerializer serializer = XmlUtils.serializerInstance();
             final XmlSerializer serializer = new FastXmlSerializer();
             serializer.setOutput(bos, "utf-8");
             serializer.startDocument(null, true);
             serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
 
             serializer.startTag(null, TAG_RESTRICTIONS);
-
-            for (String key : restrictions.keySet()) {
-                Object value = restrictions.get(key);
-                serializer.startTag(null, TAG_ENTRY);
-                serializer.attribute(null, ATTR_KEY, key);
-
-                if (value instanceof Boolean) {
-                    serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_BOOLEAN);
-                    serializer.text(value.toString());
-                } else if (value instanceof Integer) {
-                    serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_INTEGER);
-                    serializer.text(value.toString());
-                } else if (value == null || value instanceof String) {
-                    serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_STRING);
-                    serializer.text(value != null ? (String) value : "");
-                } else {
-                    serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_STRING_ARRAY);
-                    String[] values = (String[]) value;
-                    serializer.attribute(null, ATTR_MULTIPLE, Integer.toString(values.length));
-                    for (String choice : values) {
-                        serializer.startTag(null, TAG_VALUE);
-                        serializer.text(choice != null ? choice : "");
-                        serializer.endTag(null, TAG_VALUE);
-                    }
-                }
-                serializer.endTag(null, TAG_ENTRY);
-            }
-
+            writeBundle(restrictions, serializer);
             serializer.endTag(null, TAG_RESTRICTIONS);
 
             serializer.endDocument();
             restrictionsFile.finishWrite(fos);
         } catch (Exception e) {
             restrictionsFile.failWrite(fos);
-            Slog.e(LOG_TAG, "Error writing application restrictions list");
+            Slog.e(LOG_TAG, "Error writing application restrictions list", e);
+        }
+    }
+
+    private static void writeBundle(Bundle restrictions, XmlSerializer serializer)
+            throws IOException {
+        for (String key : restrictions.keySet()) {
+            Object value = restrictions.get(key);
+            serializer.startTag(null, TAG_ENTRY);
+            serializer.attribute(null, ATTR_KEY, key);
+
+            if (value instanceof Boolean) {
+                serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_BOOLEAN);
+                serializer.text(value.toString());
+            } else if (value instanceof Integer) {
+                serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_INTEGER);
+                serializer.text(value.toString());
+            } else if (value == null || value instanceof String) {
+                serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_STRING);
+                serializer.text(value != null ? (String) value : "");
+            } else if (value instanceof Bundle) {
+                serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_BUNDLE);
+                writeBundle((Bundle) value, serializer);
+            } else if (value instanceof Parcelable[]) {
+                serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_BUNDLE_ARRAY);
+                Parcelable[] array = (Parcelable[]) value;
+                for (Parcelable parcelable : array) {
+                    if (!(parcelable instanceof Bundle)) {
+                        throw new IllegalArgumentException("bundle-array can only hold Bundles");
+                    }
+                    serializer.startTag(null, TAG_ENTRY);
+                    serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_BUNDLE);
+                    writeBundle((Bundle) parcelable, serializer);
+                    serializer.endTag(null, TAG_ENTRY);
+                }
+            } else {
+                serializer.attribute(null, ATTR_VALUE_TYPE, ATTR_TYPE_STRING_ARRAY);
+                String[] values = (String[]) value;
+                serializer.attribute(null, ATTR_MULTIPLE, Integer.toString(values.length));
+                for (String choice : values) {
+                    serializer.startTag(null, TAG_VALUE);
+                    serializer.text(choice != null ? choice : "");
+                    serializer.endTag(null, TAG_VALUE);
+                }
+            }
+            serializer.endTag(null, TAG_ENTRY);
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
new file mode 100644
index 0000000..eb7eb15
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 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 com.android.server.pm;
+
+import android.os.Bundle;
+import android.os.FileUtils;
+import android.os.Parcelable;
+import android.test.AndroidTestCase;
+import android.util.AtomicFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+public class UserManagerServiceTest extends AndroidTestCase {
+    private static String[] STRING_ARRAY = new String[] {"<tag", "<![CDATA["};
+    private File restrictionsFile;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        restrictionsFile = new File(mContext.getCacheDir(), "restrictions.xml");
+        restrictionsFile.delete();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        restrictionsFile.delete();
+        super.tearDown();
+    }
+
+    public void testWriteReadApplicationRestrictions() throws IOException {
+        AtomicFile atomicFile = new AtomicFile(restrictionsFile);
+        Bundle bundle = createBundle();
+        UserManagerService.writeApplicationRestrictionsLocked(bundle, atomicFile);
+        assertTrue(atomicFile.getBaseFile().exists());
+        String s = FileUtils.readTextFile(restrictionsFile, 10000, "");
+        System.out.println("restrictionsFile: " + s);
+        bundle = UserManagerService.readApplicationRestrictionsLocked(atomicFile);
+        System.out.println("readApplicationRestrictionsLocked bundle: " + bundle);
+        assertBundle(bundle);
+    }
+
+    private Bundle createBundle() {
+        Bundle result = new Bundle();
+        // Tests for 6 allowed types: Integer, Boolean, String, String[], Bundle and Parcelable[]
+        result.putBoolean("boolean_0", false);
+        result.putBoolean("boolean_1", true);
+        result.putInt("integer", 100);
+        result.putString("empty", "");
+        result.putString("string", "text");
+        result.putStringArray("string[]", STRING_ARRAY);
+
+        Bundle bundle = new Bundle();
+        bundle.putString("bundle_string", "bundle_string");
+        bundle.putInt("bundle_int", 1);
+        result.putBundle("bundle", bundle);
+
+        Bundle[] bundleArray = new Bundle[2];
+        bundleArray[0] = new Bundle();
+        bundleArray[0].putString("bundle_array_string", "bundle_array_string");
+        bundleArray[0].putBundle("bundle_array_bundle", bundle);
+        bundleArray[1] = new Bundle();
+        bundleArray[1].putString("bundle_array_string2", "bundle_array_string2");
+        result.putParcelableArray("bundle_array", bundleArray);
+        return result;
+    }
+
+    private void assertBundle(Bundle bundle) {
+        assertFalse(bundle.getBoolean("boolean_0"));
+        assertTrue(bundle.getBoolean("boolean_1"));
+        assertEquals(100, bundle.getInt("integer"));
+        assertEquals("", bundle.getString("empty"));
+        assertEquals("text", bundle.getString("string"));
+        assertEquals(Arrays.asList(STRING_ARRAY), Arrays.asList(bundle.getStringArray("string[]")));
+        Parcelable[] bundle_array = bundle.getParcelableArray("bundle_array");
+        assertEquals(2, bundle_array.length);
+        Bundle bundle1 = (Bundle) bundle_array[0];
+        assertEquals("bundle_array_string", bundle1.getString("bundle_array_string"));
+        assertNotNull(bundle1.getBundle("bundle_array_bundle"));
+        Bundle bundle2 = (Bundle) bundle_array[1];
+        assertEquals("bundle_array_string2", bundle2.getString("bundle_array_string2"));
+        Bundle childBundle = bundle.getBundle("bundle");
+        assertEquals("bundle_string", childBundle.getString("bundle_string"));
+        assertEquals(1, childBundle.getInt("bundle_int"));
+    }
+
+}