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.compat.annotation.UnsupportedAppUsage; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageManager; 25 import android.content.pm.PackageManager.NameNotFoundException; 26 import android.content.res.TypedArray; 27 import android.content.res.XmlResourceParser; 28 import android.os.Build; 29 import android.os.Bundle; 30 import android.os.PersistableBundle; 31 import android.os.RemoteException; 32 import android.service.restrictions.RestrictionsReceiver; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.util.Xml; 36 37 import com.android.internal.R; 38 import com.android.internal.util.XmlUtils; 39 40 import org.xmlpull.v1.XmlPullParser; 41 import org.xmlpull.v1.XmlPullParserException; 42 43 import java.io.IOException; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import 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 * <?xml version="1.0" encoding="utf-8"?> 74 * <restrictions xmlns:android="http://schemas.android.com/apk/res/android" > 75 * <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" > 85 * <restriction ... /> 86 * ... 87 * </restriction> 88 * <restriction ... /> 89 * ... 90 * </restrictions> 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 * <application ... > 115 * <meta-data android:name="android.content.APP_RESTRICTIONS" 116 * android:resource="@xml/app_restrictions" /> 117 * ... 118 * </application> 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) 127 public 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 */ RestrictionsManager(Context context, IRestrictionsManager service)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 */ getApplicationRestrictions()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 */ hasRestrictionsProvider()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 */ requestPermission(String requestType, String requestId, PersistableBundle request)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 createLocalApprovalIntent()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 */ notifyPermissionResponse(String packageName, PersistableBundle response)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 */ getManifestRestrictions(String packageName)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 loadManifestRestrictions(String packageName, XmlResourceParser xml)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 loadRestrictionElement(Context appContext, XmlResourceParser xml)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 loadRestriction(Context appContext, TypedArray a, XmlResourceParser xml)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 */ convertRestrictionsToBundle(List<RestrictionEntry> entries)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 addRestrictionToBundle(Bundle bundle, RestrictionEntry entry)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 } 752