From 262f9952e6e78e00a6d42bab97d73dccfb9607f4 Mon Sep 17 00:00:00 2001 From: Fyodor Kupolov Date: Mon, 23 Mar 2015 18:55:11 -0700 Subject: [PATCH] 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 --- api/current.txt | 6 + api/system-current.txt | 6 + .../android/content/RestrictionEntry.java | 177 ++++++++++----- .../android/content/RestrictionsManager.java | 130 +++++++++-- core/res/res/values/attrs.xml | 2 + core/tests/coretests/AndroidManifest.xml | 3 + core/tests/coretests/res/values/strings.xml | 4 + .../coretests/res/xml/app_restrictions.xml | 55 +++++ .../content/RestrictionsManagerTest.java | 77 +++++++ .../android/server/pm/UserManagerService.java | 211 +++++++++++------- .../server/pm/UserManagerServiceTest.java | 102 +++++++++ 11 files changed, 621 insertions(+), 152 deletions(-) create mode 100644 core/tests/coretests/res/xml/app_restrictions.xml create mode 100644 core/tests/coretests/src/android/content/RestrictionsManagerTest.java create mode 100644 services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java diff --git a/api/current.txt b/api/current.txt index 5371ca888910d..8dd8f20670a52 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8377,6 +8377,7 @@ package android.content { 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 @@ package android.content { 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 @@ package android.content { 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 @@ package android.content { method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator 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 @@ package android.content { } public class RestrictionsManager { + method public static android.os.Bundle convertRestrictionsToBundle(java.util.List); method public android.content.Intent createLocalApprovalIntent(); method public android.os.Bundle getApplicationRestrictions(); method public java.util.List getManifestRestrictions(java.lang.String); diff --git a/api/system-current.txt b/api/system-current.txt index 3b573338a578d..b1b5cf3bcaf6e 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -8598,6 +8598,7 @@ package android.content { 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 @@ package android.content { 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 @@ package android.content { 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 @@ package android.content { method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator 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 @@ package android.content { } public class RestrictionsManager { + method public static android.os.Bundle convertRestrictionsToBundle(java.util.List); method public android.content.Intent createLocalApprovalIntent(); method public android.os.Bundle getApplicationRestrictions(); method public java.util.List getManifestRestrictions(java.lang.String); diff --git a/core/java/android/content/RestrictionEntry.java b/core/java/android/content/RestrictionEntry.java index 6d796269077c0..342ee387d469d 100644 --- a/core/java/android/content/RestrictionEntry.java +++ b/core/java/android/content/RestrictionEntry.java @@ -20,6 +20,9 @@ import android.annotation.ArrayRes; 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 @@ import android.os.Parcelable; 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 class RestrictionEntry implements Parcelable { 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 class RestrictionEntry implements Parcelable { 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 class RestrictionEntry implements Parcelable { 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 @@ public class RestrictionEntry implements Parcelable { 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; @@ -114,6 +129,12 @@ public class RestrictionEntry implements Parcelable { /* List of selected choices in the multi-select case. */ 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; * @@ -169,6 +190,35 @@ public class RestrictionEntry implements Parcelable { setIntValue(selectedInt); } + /** + * 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. @@ -282,6 +332,22 @@ public class RestrictionEntry implements Parcelable { mChoiceValues = context.getResources().getStringArray(stringArrayResId); } + /** + * 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. + *

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 @@ public class RestrictionEntry implements Parcelable { 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 @@ public class RestrictionEntry implements Parcelable { 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 @@ public class RestrictionEntry implements Parcelable { 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 CREATOR = new Creator() { @@ -461,6 +520,16 @@ public class RestrictionEntry implements Parcelable { @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 21a6a0ddad5bf..1fac06e23da1d 100644 --- a/core/java/android/content/RestrictionsManager.java +++ b/core/java/android/content/RestrictionsManager.java @@ -32,12 +32,14 @@ import android.util.Log; 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 @@ import java.util.List; * 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" - * /> + * android:defaultValue="reference" > + * <restriction ... /> + * ... + * </restriction> * <restriction ... /> * ... * </restrictions> @@ -97,6 +102,9 @@ import java.util.List; * administrator controlling the values, if the title is not sufficient. * *

+ * Only restrictions of type {@code bundle} and {@code bundle_array} can have one or multiple nested + * restriction elements. + *

* In your manifest's application section, add the meta-data tag to point to * the restrictions XML file as shown below: *

@@ -537,9 +545,7 @@ public class RestrictionsManager {
 
         XmlResourceParser xml =
                 appInfo.loadXmlMetaData(mContext.getPackageManager(), META_DATA_APP_RESTRICTIONS);
-        List restrictions = loadManifestRestrictions(packageName, xml);
-
-        return restrictions;
+        return loadManifestRestrictions(packageName, xml);
     }
 
     private List loadManifestRestrictions(String packageName,
@@ -550,23 +556,16 @@ public class RestrictionsManager {
         } catch (NameNotFoundException nnfe) {
             return null;
         }
-        ArrayList restrictions = new ArrayList();
+        ArrayList 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 @@ public class RestrictionsManager {
         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 @@ public class RestrictionsManager {
                 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 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:
+     * 
+     *     
+     *     
+     *     
+     *     
+     *     
+     *     
+     *     
+     *     
+     *     
+     * 
RestrictionEntryBundle
{@link RestrictionEntry#TYPE_BOOLEAN}{@link Bundle#putBoolean}
{@link RestrictionEntry#TYPE_CHOICE}, {@link RestrictionEntry#TYPE_CHOICE}{@link Bundle#putStringArray}
{@link RestrictionEntry#TYPE_INTEGER}{@link Bundle#putInt}
{@link RestrictionEntry#TYPE_STRING}{@link Bundle#putString}
{@link RestrictionEntry#TYPE_BUNDLE}{@link Bundle#putBundle}
{@link RestrictionEntry#TYPE_BUNDLE_ARRAY}{@link Bundle#putParcelableArray}
+ * @param entries list of restrictions + */ + public static Bundle convertRestrictionsToBundle(List 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 dcb4b9e760ce2..b6d32b2dfac46 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7759,6 +7759,8 @@ + + diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index bfaea8fa70860..b07d3383362cd 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -113,6 +113,9 @@ + diff --git a/core/tests/coretests/res/values/strings.xml b/core/tests/coretests/res/values/strings.xml index ce0d9a248226f..04b0478c96d11 100644 --- a/core/tests/coretests/res/values/strings.xml +++ b/core/tests/coretests/res/values/strings.xml @@ -135,4 +135,8 @@ 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. Abe Lincoln Lincoln adressing the crowd at Gettysburgh + + + Title + Description 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 0000000000000..c84cabcc00d3e --- /dev/null +++ b/core/tests/coretests/res/xml/app_restrictions.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + \ 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 0000000000000..8921924c5081f --- /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 manifestRestrictions = mRm.getManifestRestrictions(packageName); + assertEquals(6, manifestRestrictions.size()); + Set 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 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 8cc9d192caa66..5e58cd9389ec0 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.Handler; 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.SparseBooleanArray; 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.security.SecureRandom; 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 @@ public class UserManagerService extends IUserManager.Stub { 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 @@ public class UserManagerService extends IUserManager.Stub { 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 values = new ArrayList(); + final ArrayList 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 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 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 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 0000000000000..eb7eb15a491ac --- /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[] {"