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