blob: 885eb708d92cd08163beb9e7a05c087ba213eee9 [file] [log] [blame]
Aurimas Liutikas93554f22022-04-19 16:51:35 -07001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
19import android.annotation.SystemService;
20import android.app.Activity;
21import android.app.admin.DevicePolicyManager;
22import android.compat.annotation.UnsupportedAppUsage;
23import android.content.pm.ApplicationInfo;
24import android.content.pm.PackageManager;
25import android.content.pm.PackageManager.NameNotFoundException;
26import android.content.res.TypedArray;
27import android.content.res.XmlResourceParser;
28import android.os.Build;
29import android.os.Bundle;
30import android.os.PersistableBundle;
31import android.os.RemoteException;
32import android.service.restrictions.RestrictionsReceiver;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.util.Xml;
36
37import com.android.internal.R;
38import com.android.internal.util.XmlUtils;
39
40import org.xmlpull.v1.XmlPullParser;
41import org.xmlpull.v1.XmlPullParserException;
42
43import java.io.IOException;
44import java.util.ArrayList;
45import java.util.Arrays;
46import java.util.List;
47
48/**
49 * Provides a mechanism for apps to query restrictions imposed by an entity that
50 * manages the user. Apps can also send permission requests to a local or remote
51 * device administrator to override default app-specific restrictions or any other
52 * operation that needs explicit authorization from the administrator.
53 * <p>
54 * Apps can expose a set of restrictions via an XML file specified in the manifest.
55 * <p>
56 * If the user has an active Restrictions Provider, dynamic requests can be made in
57 * addition to the statically imposed restrictions. Dynamic requests are app-specific
58 * and can be expressed via a predefined set of request types.
59 * <p>
60 * The RestrictionsManager forwards the dynamic requests to the active
61 * Restrictions Provider. The Restrictions Provider can respond back to requests by calling
62 * {@link #notifyPermissionResponse(String, PersistableBundle)}, when
63 * a response is received from the administrator of the device or user.
64 * The response is relayed back to the application via a protected broadcast,
65 * {@link #ACTION_PERMISSION_RESPONSE_RECEIVED}.
66 * <p>
67 * Static restrictions are specified by an XML file referenced by a meta-data attribute
68 * in the manifest. This enables applications as well as any web administration consoles
69 * to be able to read the list of available restrictions from the apk.
70 * <p>
71 * The syntax of the XML format is as follows:
72 * <pre>
73 * &lt;?xml version="1.0" encoding="utf-8"?&gt;
74 * &lt;restrictions xmlns:android="http://schemas.android.com/apk/res/android" &gt;
75 * &lt;restriction
76 * android:key="string"
77 * android:title="string resource"
78 * android:restrictionType=["bool" | "string" | "integer"
79 * | "choice" | "multi-select" | "hidden"
80 * | "bundle" | "bundle_array"]
81 * android:description="string resource"
82 * android:entries="string-array resource"
83 * android:entryValues="string-array resource"
84 * android:defaultValue="reference" &gt;
85 * &lt;restriction ... /&gt;
86 * ...
87 * &lt;/restriction&gt;
88 * &lt;restriction ... /&gt;
89 * ...
90 * &lt;/restrictions&gt;
91 * </pre>
92 * <p>
93 * The attributes for each restriction depend on the restriction type.
94 * <p>
95 * <ul>
96 * <li><code>key</code>, <code>title</code> and <code>restrictionType</code> are mandatory.</li>
97 * <li><code>entries</code> and <code>entryValues</code> are required if <code>restrictionType
98 * </code> is <code>choice</code> or <code>multi-select</code>.</li>
99 * <li><code>defaultValue</code> is optional and its type depends on the
100 * <code>restrictionType</code></li>
101 * <li><code>hidden</code> type must have a <code>defaultValue</code> and will
102 * not be shown to the administrator. It can be used to pass along data that cannot be modified,
103 * such as a version code.</li>
104 * <li><code>description</code> is meant to describe the restriction in more detail to the
105 * administrator controlling the values, if the title is not sufficient.</li>
106 * </ul>
107 * <p>
108 * Only restrictions of type {@code bundle} and {@code bundle_array} can have one or multiple nested
109 * restriction elements.
110 * <p>
111 * In your manifest's <code>application</code> section, add the meta-data tag to point to
112 * the restrictions XML file as shown below:
113 * <pre>
114 * &lt;application ... &gt;
115 * &lt;meta-data android:name="android.content.APP_RESTRICTIONS"
116 * android:resource="@xml/app_restrictions" /&gt;
117 * ...
118 * &lt;/application&gt;
119 * </pre>
120 *
121 * @see RestrictionEntry
122 * @see RestrictionsReceiver
123 * @see DevicePolicyManager#setRestrictionsProvider(ComponentName, ComponentName)
124 * @see DevicePolicyManager#setApplicationRestrictions(ComponentName, String, Bundle)
125 */
126@SystemService(Context.RESTRICTIONS_SERVICE)
127public class RestrictionsManager {
128
129 private static final String TAG = "RestrictionsManager";
130
131 /**
132 * Broadcast intent delivered when a response is received for a permission request. The
133 * application should not interrupt the user by coming to the foreground if it isn't
134 * currently in the foreground. It can either post a notification informing
135 * the user of the response or wait until the next time the user launches the app.
136 * <p>
137 * For instance, if the user requested permission to make an in-app purchase,
138 * the app can post a notification that the request had been approved or denied.
139 * <p>
140 * The broadcast Intent carries the following extra:
141 * {@link #EXTRA_RESPONSE_BUNDLE}.
142 */
143 public static final String ACTION_PERMISSION_RESPONSE_RECEIVED =
144 "android.content.action.PERMISSION_RESPONSE_RECEIVED";
145
146 /**
147 * Broadcast intent sent to the Restrictions Provider to handle a permission request from
148 * an app. It will have the following extras: {@link #EXTRA_PACKAGE_NAME},
149 * {@link #EXTRA_REQUEST_TYPE}, {@link #EXTRA_REQUEST_ID} and {@link #EXTRA_REQUEST_BUNDLE}.
150 * The Restrictions Provider will handle the request and respond back to the
151 * RestrictionsManager, when a response is available, by calling
152 * {@link #notifyPermissionResponse}.
153 * <p>
154 * The BroadcastReceiver must require the {@link android.Manifest.permission#BIND_DEVICE_ADMIN}
155 * permission to ensure that only the system can send the broadcast.
156 */
157 public static final String ACTION_REQUEST_PERMISSION =
158 "android.content.action.REQUEST_PERMISSION";
159
160 /**
161 * Activity intent that is optionally implemented by the Restrictions Provider package
162 * to challenge for an administrator PIN or password locally on the device. Apps will
163 * call this intent using {@link Activity#startActivityForResult}. On a successful
164 * response, {@link Activity#onActivityResult} will return a resultCode of
165 * {@link Activity#RESULT_OK}.
166 * <p>
167 * The intent must contain {@link #EXTRA_REQUEST_BUNDLE} as an extra and the bundle must
168 * contain at least {@link #REQUEST_KEY_MESSAGE} for the activity to display.
169 * <p>
170 * @see #createLocalApprovalIntent()
171 */
172 public static final String ACTION_REQUEST_LOCAL_APPROVAL =
173 "android.content.action.REQUEST_LOCAL_APPROVAL";
174
175 /**
176 * The package name of the application making the request.
177 * <p>
178 * Type: String
179 */
180 public static final String EXTRA_PACKAGE_NAME = "android.content.extra.PACKAGE_NAME";
181
182 /**
183 * The request type passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
184 * <p>
185 * Type: String
186 */
187 public static final String EXTRA_REQUEST_TYPE = "android.content.extra.REQUEST_TYPE";
188
189 /**
190 * The request ID passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
191 * <p>
192 * Type: String
193 */
194 public static final String EXTRA_REQUEST_ID = "android.content.extra.REQUEST_ID";
195
196 /**
197 * The request bundle passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
198 * <p>
199 * Type: {@link PersistableBundle}
200 */
201 public static final String EXTRA_REQUEST_BUNDLE = "android.content.extra.REQUEST_BUNDLE";
202
203 /**
204 * Contains a response from the administrator for specific request.
205 * The bundle contains the following information, at least:
206 * <ul>
207 * <li>{@link #REQUEST_KEY_ID}: The request ID.</li>
208 * <li>{@link #RESPONSE_KEY_RESULT}: The response result.</li>
209 * </ul>
210 * <p>
211 * Type: {@link PersistableBundle}
212 */
213 public static final String EXTRA_RESPONSE_BUNDLE = "android.content.extra.RESPONSE_BUNDLE";
214
215 /**
216 * Request type for a simple question, with a possible title and icon.
217 * <p>
218 * Required keys are: {@link #REQUEST_KEY_MESSAGE}
219 * <p>
220 * Optional keys are
221 * {@link #REQUEST_KEY_DATA}, {@link #REQUEST_KEY_ICON}, {@link #REQUEST_KEY_TITLE},
222 * {@link #REQUEST_KEY_APPROVE_LABEL} and {@link #REQUEST_KEY_DENY_LABEL}.
223 */
224 public static final String REQUEST_TYPE_APPROVAL = "android.request.type.approval";
225
226 /**
227 * Key for request ID contained in the request bundle.
228 * <p>
229 * App-generated request ID to identify the specific request when receiving
230 * a response. This value is returned in the {@link #EXTRA_RESPONSE_BUNDLE}.
231 * <p>
232 * Type: String
233 */
234 public static final String REQUEST_KEY_ID = "android.request.id";
235
236 /**
237 * Key for request data contained in the request bundle.
238 * <p>
239 * Optional, typically used to identify the specific data that is being referred to,
240 * such as the unique identifier for a movie or book. This is not used for display
241 * purposes and is more like a cookie. This value is returned in the
242 * {@link #EXTRA_RESPONSE_BUNDLE}.
243 * <p>
244 * Type: String
245 */
246 public static final String REQUEST_KEY_DATA = "android.request.data";
247
248 /**
249 * Key for request title contained in the request bundle.
250 * <p>
251 * Optional, typically used as the title of any notification or dialog presented
252 * to the administrator who approves the request.
253 * <p>
254 * Type: String
255 */
256 public static final String REQUEST_KEY_TITLE = "android.request.title";
257
258 /**
259 * Key for request message contained in the request bundle.
260 * <p>
261 * Required, shown as the actual message in a notification or dialog presented
262 * to the administrator who approves the request.
263 * <p>
264 * Type: String
265 */
266 public static final String REQUEST_KEY_MESSAGE = "android.request.mesg";
267
268 /**
269 * Key for request icon contained in the request bundle.
270 * <p>
271 * Optional, shown alongside the request message presented to the administrator
272 * who approves the request. The content must be a compressed image such as a
273 * PNG or JPEG, as a byte array.
274 * <p>
275 * Type: byte[]
276 */
277 public static final String REQUEST_KEY_ICON = "android.request.icon";
278
279 /**
280 * Key for request approval button label contained in the request bundle.
281 * <p>
282 * Optional, may be shown as a label on the positive button in a dialog or
283 * notification presented to the administrator who approves the request.
284 * <p>
285 * Type: String
286 */
287 public static final String REQUEST_KEY_APPROVE_LABEL = "android.request.approve_label";
288
289 /**
290 * Key for request rejection button label contained in the request bundle.
291 * <p>
292 * Optional, may be shown as a label on the negative button in a dialog or
293 * notification presented to the administrator who approves the request.
294 * <p>
295 * Type: String
296 */
297 public static final String REQUEST_KEY_DENY_LABEL = "android.request.deny_label";
298
299 /**
300 * Key for issuing a new request, contained in the request bundle. If this is set to true,
301 * the Restrictions Provider must make a new request. If it is false or not specified, then
302 * the Restrictions Provider can return a cached response that has the same requestId, if
303 * available. If there's no cached response, it will issue a new one to the administrator.
304 * <p>
305 * Type: boolean
306 */
307 public static final String REQUEST_KEY_NEW_REQUEST = "android.request.new_request";
308
309 /**
310 * Key for the response result in the response bundle sent to the application, for a permission
311 * request. It indicates the status of the request. In some cases an additional message might
312 * be available in {@link #RESPONSE_KEY_MESSAGE}, to be displayed to the user.
313 * <p>
314 * Type: int
315 * <p>
316 * Possible values: {@link #RESULT_APPROVED}, {@link #RESULT_DENIED},
317 * {@link #RESULT_NO_RESPONSE}, {@link #RESULT_UNKNOWN_REQUEST} or
318 * {@link #RESULT_ERROR}.
319 */
320 public static final String RESPONSE_KEY_RESULT = "android.response.result";
321
322 /**
323 * Response result value indicating that the request was approved.
324 */
325 public static final int RESULT_APPROVED = 1;
326
327 /**
328 * Response result value indicating that the request was denied.
329 */
330 public static final int RESULT_DENIED = 2;
331
332 /**
333 * Response result value indicating that the request has not received a response yet.
334 */
335 public static final int RESULT_NO_RESPONSE = 3;
336
337 /**
338 * Response result value indicating that the request is unknown, when it's not a new
339 * request.
340 */
341 public static final int RESULT_UNKNOWN_REQUEST = 4;
342
343 /**
344 * Response result value indicating an error condition. Additional error code might be available
345 * in the response bundle, for the key {@link #RESPONSE_KEY_ERROR_CODE}. There might also be
346 * an associated error message in the response bundle, for the key
347 * {@link #RESPONSE_KEY_MESSAGE}.
348 */
349 public static final int RESULT_ERROR = 5;
350
351 /**
352 * Error code indicating that there was a problem with the request.
353 * <p>
354 * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
355 */
356 public static final int RESULT_ERROR_BAD_REQUEST = 1;
357
358 /**
359 * Error code indicating that there was a problem with the network.
360 * <p>
361 * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
362 */
363 public static final int RESULT_ERROR_NETWORK = 2;
364
365 /**
366 * Error code indicating that there was an internal error.
367 * <p>
368 * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
369 */
370 public static final int RESULT_ERROR_INTERNAL = 3;
371
372 /**
373 * Key for the optional error code in the response bundle sent to the application.
374 * <p>
375 * Type: int
376 * <p>
377 * Possible values: {@link #RESULT_ERROR_BAD_REQUEST}, {@link #RESULT_ERROR_NETWORK} or
378 * {@link #RESULT_ERROR_INTERNAL}.
379 */
380 public static final String RESPONSE_KEY_ERROR_CODE = "android.response.errorcode";
381
382 /**
383 * Key for the optional message in the response bundle sent to the application.
384 * <p>
385 * Type: String
386 */
387 public static final String RESPONSE_KEY_MESSAGE = "android.response.msg";
388
389 /**
390 * Key for the optional timestamp of when the administrator responded to the permission
391 * request. It is an represented in milliseconds since January 1, 1970 00:00:00.0 UTC.
392 * <p>
393 * Type: long
394 */
395 public static final String RESPONSE_KEY_RESPONSE_TIMESTAMP = "android.response.timestamp";
396
397 /**
398 * Name of the meta-data entry in the manifest that points to the XML file containing the
399 * application's available restrictions.
400 * @see #getManifestRestrictions(String)
401 */
402 public static final String META_DATA_APP_RESTRICTIONS = "android.content.APP_RESTRICTIONS";
403
404 private static final String TAG_RESTRICTION = "restriction";
405
406 private final Context mContext;
407 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
408 private final IRestrictionsManager mService;
409
410 /**
411 * @hide
412 */
413 public RestrictionsManager(Context context, IRestrictionsManager service) {
414 mContext = context;
415 mService = service;
416 }
417
418 /**
419 * Returns any available set of application-specific restrictions applicable
420 * to this application.
421 * @return the application restrictions as a Bundle. Returns null if there
422 * are no restrictions.
423 */
424 public Bundle getApplicationRestrictions() {
425 try {
426 if (mService != null) {
427 return mService.getApplicationRestrictions(mContext.getPackageName());
428 }
429 } catch (RemoteException re) {
430 throw re.rethrowFromSystemServer();
431 }
432 return null;
433 }
434
435 /**
436 * Called by an application to check if there is an active Restrictions Provider. If
437 * there isn't, {@link #requestPermission(String, String, PersistableBundle)} is not available.
438 *
439 * @return whether there is an active Restrictions Provider.
440 */
441 public boolean hasRestrictionsProvider() {
442 try {
443 if (mService != null) {
444 return mService.hasRestrictionsProvider();
445 }
446 } catch (RemoteException re) {
447 throw re.rethrowFromSystemServer();
448 }
449 return false;
450 }
451
452 /**
453 * Called by an application to request permission for an operation. The contents of the
454 * request are passed in a Bundle that contains several pieces of data depending on the
455 * chosen request type.
456 *
457 * @param requestType The type of request. The type could be one of the
458 * predefined types specified here or a custom type that the specific
459 * Restrictions Provider might understand. For custom types, the type name should be
460 * namespaced to avoid collisions with predefined types and types specified by
461 * other Restrictions Providers.
462 * @param requestId A unique id generated by the app that contains sufficient information
463 * to identify the parameters of the request when it receives the id in the response.
464 * @param request A PersistableBundle containing the data corresponding to the specified request
465 * type. The keys for the data in the bundle depend on the request type.
466 *
467 * @throws IllegalArgumentException if any of the required parameters are missing.
468 */
469 public void requestPermission(String requestType, String requestId, PersistableBundle request) {
470 if (requestType == null) {
471 throw new NullPointerException("requestType cannot be null");
472 }
473 if (requestId == null) {
474 throw new NullPointerException("requestId cannot be null");
475 }
476 if (request == null) {
477 throw new NullPointerException("request cannot be null");
478 }
479 try {
480 if (mService != null) {
481 mService.requestPermission(mContext.getPackageName(), requestType, requestId,
482 request);
483 }
484 } catch (RemoteException re) {
485 throw re.rethrowFromSystemServer();
486 }
487 }
488
489 public Intent createLocalApprovalIntent() {
490 try {
491 if (mService != null) {
492 return mService.createLocalApprovalIntent();
493 }
494 } catch (RemoteException re) {
495 throw re.rethrowFromSystemServer();
496 }
497 return null;
498 }
499
500 /**
501 * Called by the Restrictions Provider to deliver a response to an application.
502 *
503 * @param packageName the application to deliver the response to. Cannot be null.
504 * @param response the bundle containing the response status, request ID and other information.
505 * Cannot be null.
506 *
507 * @throws IllegalArgumentException if any of the required parameters are missing.
508 */
509 public void notifyPermissionResponse(String packageName, PersistableBundle response) {
510 if (packageName == null) {
511 throw new NullPointerException("packageName cannot be null");
512 }
513 if (response == null) {
514 throw new NullPointerException("request cannot be null");
515 }
516 if (!response.containsKey(REQUEST_KEY_ID)) {
517 throw new IllegalArgumentException("REQUEST_KEY_ID must be specified");
518 }
519 if (!response.containsKey(RESPONSE_KEY_RESULT)) {
520 throw new IllegalArgumentException("RESPONSE_KEY_RESULT must be specified");
521 }
522 try {
523 if (mService != null) {
524 mService.notifyPermissionResponse(packageName, response);
525 }
526 } catch (RemoteException re) {
527 throw re.rethrowFromSystemServer();
528 }
529 }
530
531 /**
532 * Parse and return the list of restrictions defined in the manifest for the specified
533 * package, if any.
534 *
535 * @param packageName The application for which to fetch the restrictions list.
536 * @return The list of RestrictionEntry objects created from the XML file specified
537 * in the manifest, or null if none was specified.
538 */
539 public List<RestrictionEntry> getManifestRestrictions(String packageName) {
540 ApplicationInfo appInfo = null;
541 try {
542 appInfo = mContext.getPackageManager().getApplicationInfo(packageName,
543 PackageManager.GET_META_DATA);
544 } catch (NameNotFoundException pnfe) {
545 throw new IllegalArgumentException("No such package " + packageName);
546 }
547 if (appInfo == null || !appInfo.metaData.containsKey(META_DATA_APP_RESTRICTIONS)) {
548 return null;
549 }
550
551 XmlResourceParser xml =
552 appInfo.loadXmlMetaData(mContext.getPackageManager(), META_DATA_APP_RESTRICTIONS);
553 return loadManifestRestrictions(packageName, xml);
554 }
555
556 private List<RestrictionEntry> loadManifestRestrictions(String packageName,
557 XmlResourceParser xml) {
558 Context appContext;
559 try {
560 appContext = mContext.createPackageContext(packageName, 0 /* flags */);
561 } catch (NameNotFoundException nnfe) {
562 return null;
563 }
564 ArrayList<RestrictionEntry> restrictions = new ArrayList<>();
565 RestrictionEntry restriction;
566
567 try {
568 int tagType = xml.next();
569 while (tagType != XmlPullParser.END_DOCUMENT) {
570 if (tagType == XmlPullParser.START_TAG) {
571 restriction = loadRestrictionElement(appContext, xml);
572 if (restriction != null) {
573 restrictions.add(restriction);
574 }
575 }
576 tagType = xml.next();
577 }
578 } catch (XmlPullParserException e) {
579 Log.w(TAG, "Reading restriction metadata for " + packageName, e);
580 return null;
581 } catch (IOException e) {
582 Log.w(TAG, "Reading restriction metadata for " + packageName, e);
583 return null;
584 }
585
586 return restrictions;
587 }
588
589 private RestrictionEntry loadRestrictionElement(Context appContext, XmlResourceParser xml)
590 throws IOException, XmlPullParserException {
591 if (xml.getName().equals(TAG_RESTRICTION)) {
592 AttributeSet attrSet = Xml.asAttributeSet(xml);
593 if (attrSet != null) {
594 TypedArray a = appContext.obtainStyledAttributes(attrSet,
595 com.android.internal.R.styleable.RestrictionEntry);
596 return loadRestriction(appContext, a, xml);
597 }
598 }
599 return null;
600 }
601
602 private RestrictionEntry loadRestriction(Context appContext, TypedArray a, XmlResourceParser xml)
603 throws IOException, XmlPullParserException {
604 String key = a.getString(R.styleable.RestrictionEntry_key);
605 int restrictionType = a.getInt(
606 R.styleable.RestrictionEntry_restrictionType, -1);
607 String title = a.getString(R.styleable.RestrictionEntry_title);
608 String description = a.getString(R.styleable.RestrictionEntry_description);
609 int entries = a.getResourceId(R.styleable.RestrictionEntry_entries, 0);
610 int entryValues = a.getResourceId(R.styleable.RestrictionEntry_entryValues, 0);
611
612 if (restrictionType == -1) {
613 Log.w(TAG, "restrictionType cannot be omitted");
614 return null;
615 }
616
617 if (key == null) {
618 Log.w(TAG, "key cannot be omitted");
619 return null;
620 }
621
622 RestrictionEntry restriction = new RestrictionEntry(restrictionType, key);
623 restriction.setTitle(title);
624 restriction.setDescription(description);
625 if (entries != 0) {
626 restriction.setChoiceEntries(appContext, entries);
627 }
628 if (entryValues != 0) {
629 restriction.setChoiceValues(appContext, entryValues);
630 }
631 // Extract the default value based on the type
632 switch (restrictionType) {
633 case RestrictionEntry.TYPE_NULL: // hidden
634 case RestrictionEntry.TYPE_STRING:
635 case RestrictionEntry.TYPE_CHOICE:
636 restriction.setSelectedString(
637 a.getString(R.styleable.RestrictionEntry_defaultValue));
638 break;
639 case RestrictionEntry.TYPE_INTEGER:
640 restriction.setIntValue(
641 a.getInt(R.styleable.RestrictionEntry_defaultValue, 0));
642 break;
643 case RestrictionEntry.TYPE_MULTI_SELECT:
644 int resId = a.getResourceId(R.styleable.RestrictionEntry_defaultValue, 0);
645 if (resId != 0) {
646 restriction.setAllSelectedStrings(
647 appContext.getResources().getStringArray(resId));
648 }
649 break;
650 case RestrictionEntry.TYPE_BOOLEAN:
651 restriction.setSelectedState(
652 a.getBoolean(R.styleable.RestrictionEntry_defaultValue, false));
653 break;
654 case RestrictionEntry.TYPE_BUNDLE:
655 case RestrictionEntry.TYPE_BUNDLE_ARRAY:
656 final int outerDepth = xml.getDepth();
657 List<RestrictionEntry> restrictionEntries = new ArrayList<>();
658 while (XmlUtils.nextElementWithin(xml, outerDepth)) {
659 RestrictionEntry childEntry = loadRestrictionElement(appContext, xml);
660 if (childEntry == null) {
661 Log.w(TAG, "Child entry cannot be loaded for bundle restriction " + key);
662 } else {
663 restrictionEntries.add(childEntry);
664 if (restrictionType == RestrictionEntry.TYPE_BUNDLE_ARRAY
665 && childEntry.getType() != RestrictionEntry.TYPE_BUNDLE) {
666 Log.w(TAG, "bundle_array " + key
667 + " can only contain entries of type bundle");
668 }
669 }
670 }
671 restriction.setRestrictions(restrictionEntries.toArray(new RestrictionEntry[
672 restrictionEntries.size()]));
673 break;
674 default:
675 Log.w(TAG, "Unknown restriction type " + restrictionType);
676 }
677 return restriction;
678 }
679
680 /**
681 * Converts a list of restrictions to the corresponding bundle, using the following mapping:
682 * <table>
683 * <tr><th>RestrictionEntry</th><th>Bundle</th></tr>
684 * <tr><td>{@link RestrictionEntry#TYPE_BOOLEAN}</td><td>{@link Bundle#putBoolean}</td></tr>
685 * <tr><td>{@link RestrictionEntry#TYPE_CHOICE},
686 * {@link RestrictionEntry#TYPE_MULTI_SELECT}</td>
687 * <td>{@link Bundle#putStringArray}</td></tr>
688 * <tr><td>{@link RestrictionEntry#TYPE_INTEGER}</td><td>{@link Bundle#putInt}</td></tr>
689 * <tr><td>{@link RestrictionEntry#TYPE_STRING}</td><td>{@link Bundle#putString}</td></tr>
690 * <tr><td>{@link RestrictionEntry#TYPE_BUNDLE}</td><td>{@link Bundle#putBundle}</td></tr>
691 * <tr><td>{@link RestrictionEntry#TYPE_BUNDLE_ARRAY}</td>
692 * <td>{@link Bundle#putParcelableArray}</td></tr>
693 * </table>
694 * @param entries list of restrictions
695 */
696 public static Bundle convertRestrictionsToBundle(List<RestrictionEntry> entries) {
697 final Bundle bundle = new Bundle();
698 for (RestrictionEntry entry : entries) {
699 addRestrictionToBundle(bundle, entry);
700 }
701 return bundle;
702 }
703
704 private static Bundle addRestrictionToBundle(Bundle bundle, RestrictionEntry entry) {
705 switch (entry.getType()) {
706 case RestrictionEntry.TYPE_BOOLEAN:
707 bundle.putBoolean(entry.getKey(), entry.getSelectedState());
708 break;
709 case RestrictionEntry.TYPE_CHOICE:
710 case RestrictionEntry.TYPE_CHOICE_LEVEL:
711 case RestrictionEntry.TYPE_MULTI_SELECT:
712 bundle.putStringArray(entry.getKey(), entry.getAllSelectedStrings());
713 break;
714 case RestrictionEntry.TYPE_INTEGER:
715 bundle.putInt(entry.getKey(), entry.getIntValue());
716 break;
717 case RestrictionEntry.TYPE_STRING:
718 case RestrictionEntry.TYPE_NULL:
719 bundle.putString(entry.getKey(), entry.getSelectedString());
720 break;
721 case RestrictionEntry.TYPE_BUNDLE:
722 RestrictionEntry[] restrictions = entry.getRestrictions();
723 Bundle childBundle = convertRestrictionsToBundle(Arrays.asList(restrictions));
724 bundle.putBundle(entry.getKey(), childBundle);
725 break;
726 case RestrictionEntry.TYPE_BUNDLE_ARRAY:
727 RestrictionEntry[] bundleRestrictionArray = entry.getRestrictions();
728 Bundle[] bundleArray = new Bundle[bundleRestrictionArray.length];
729 for (int i = 0; i < bundleRestrictionArray.length; i++) {
730 RestrictionEntry[] bundleRestrictions =
731 bundleRestrictionArray[i].getRestrictions();
732 if (bundleRestrictions == null) {
733 // Non-bundle entry found in bundle array.
734 Log.w(TAG, "addRestrictionToBundle: " +
735 "Non-bundle entry found in bundle array");
736 bundleArray[i] = new Bundle();
737 } else {
738 bundleArray[i] = convertRestrictionsToBundle(Arrays.asList(
739 bundleRestrictions));
740 }
741 }
742 bundle.putParcelableArray(entry.getKey(), bundleArray);
743 break;
744 default:
745 throw new IllegalArgumentException(
746 "Unsupported restrictionEntry type: " + entry.getType());
747 }
748 return bundle;
749 }
750
751}