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