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
This commit is contained in:
Fyodor Kupolov
2015-03-23 18:55:11 -07:00
parent 607f1f09c3
commit 262f9952e6
11 changed files with 621 additions and 152 deletions

View File

@@ -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.
* <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 @@ 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<RestrictionEntry> CREATOR = new Creator<RestrictionEntry>() {
@@ -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) +
'}';
}
}

View File

@@ -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"
* /&gt;
* android:defaultValue="reference" &gt;
* &lt;restriction ... /&gt;
* ...
* &lt;/restriction&gt;
* &lt;restriction ... /&gt;
* ...
* &lt;/restrictions&gt;
@@ -97,6 +102,9 @@ import java.util.List;
* 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 @@ public class RestrictionsManager {
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 @@ public class RestrictionsManager {
} 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 @@ 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<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;
}
}