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