1 /* Copyright (C) 2010 The Android Open Source Project. 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package com.android.exchange.adapter; 17 18 import android.app.admin.DevicePolicyManager; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.os.Environment; 22 import android.support.v4.content.ContextCompat; 23 24 import com.android.emailcommon.provider.Policy; 25 import com.android.emailcommon.service.PolicyServiceProxy; 26 import com.android.exchange.Eas; 27 import com.android.exchange.R; 28 import com.android.exchange.eas.EasProvision; 29 import com.android.mail.utils.LogUtils; 30 31 import org.xmlpull.v1.XmlPullParser; 32 import org.xmlpull.v1.XmlPullParserException; 33 import org.xmlpull.v1.XmlPullParserFactory; 34 35 import java.io.ByteArrayInputStream; 36 import java.io.File; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.util.ArrayList; 40 41 /** 42 * Parse the result of the Provision command 43 */ 44 public class ProvisionParser extends Parser { 45 private static final String TAG = Eas.LOG_TAG; 46 47 private final Context mContext; 48 private Policy mPolicy = null; 49 private String mSecuritySyncKey = null; 50 private boolean mRemoteWipe = false; 51 private boolean mIsSupportable = true; 52 private boolean smimeRequired = false; 53 private final Resources mResources; 54 ProvisionParser(final Context context, final InputStream in)55 public ProvisionParser(final Context context, final InputStream in) throws IOException { 56 super(in); 57 mContext = context; 58 mResources = context.getResources(); 59 } 60 getPolicy()61 public Policy getPolicy() { 62 return mPolicy; 63 } 64 getSecuritySyncKey()65 public String getSecuritySyncKey() { 66 return mSecuritySyncKey; 67 } 68 setSecuritySyncKey(String securitySyncKey)69 public void setSecuritySyncKey(String securitySyncKey) { 70 mSecuritySyncKey = securitySyncKey; 71 } 72 getRemoteWipe()73 public boolean getRemoteWipe() { 74 return mRemoteWipe; 75 } 76 hasSupportablePolicySet()77 public boolean hasSupportablePolicySet() { 78 return (mPolicy != null) && mIsSupportable; 79 } 80 clearUnsupportablePolicies()81 public void clearUnsupportablePolicies() { 82 mIsSupportable = true; 83 mPolicy.mProtocolPoliciesUnsupported = null; 84 } 85 addPolicyString(StringBuilder sb, int res)86 private void addPolicyString(StringBuilder sb, int res) { 87 sb.append(mResources.getString(res)); 88 sb.append(Policy.POLICY_STRING_DELIMITER); 89 } 90 91 /** 92 * Complete setup of a Policy; we normalize it first (removing inconsistencies, etc.) and then 93 * generate the tokenized "protocol policies enforced" string. Note that unsupported policies 94 * must have been added prior to calling this method (this is only a possibility with wbxml 95 * policy documents, as all versions of the OS support the policies in xml documents). 96 */ setPolicy(Policy policy)97 private void setPolicy(Policy policy) { 98 policy.normalize(); 99 StringBuilder sb = new StringBuilder(); 100 if (policy.mDontAllowAttachments) { 101 addPolicyString(sb, R.string.policy_dont_allow_attachments); 102 } 103 if (policy.mRequireManualSyncWhenRoaming) { 104 addPolicyString(sb, R.string.policy_require_manual_sync_roaming); 105 } 106 policy.mProtocolPoliciesEnforced = sb.toString(); 107 mPolicy = policy; 108 } 109 deviceSupportsEncryption()110 private boolean deviceSupportsEncryption() { 111 DevicePolicyManager dpm = 112 (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); 113 int status = dpm.getStorageEncryptionStatus(); 114 return status != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; 115 } 116 parseProvisionDocWbxml()117 private void parseProvisionDocWbxml() throws IOException { 118 Policy policy = new Policy(); 119 ArrayList<Integer> unsupportedList = new ArrayList<Integer>(); 120 boolean passwordEnabled = false; 121 122 while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) { 123 boolean tagIsSupported = true; 124 int res = 0; 125 switch (tag) { 126 case Tags.PROVISION_DEVICE_PASSWORD_ENABLED: 127 if (getValueInt() == 1) { 128 passwordEnabled = true; 129 if (policy.mPasswordMode == Policy.PASSWORD_MODE_NONE) { 130 policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE; 131 } 132 } 133 break; 134 case Tags.PROVISION_MIN_DEVICE_PASSWORD_LENGTH: 135 policy.mPasswordMinLength = getValueInt(); 136 break; 137 case Tags.PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED: 138 if (getValueInt() == 1) { 139 policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG; 140 } 141 break; 142 case Tags.PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK: 143 // EAS gives us seconds, which is, happily, what the PolicySet requires 144 policy.mMaxScreenLockTime = getValueInt(); 145 break; 146 case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS: 147 policy.mPasswordMaxFails = getValueInt(); 148 break; 149 case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION: 150 policy.mPasswordExpirationDays = getValueInt(); 151 break; 152 case Tags.PROVISION_DEVICE_PASSWORD_HISTORY: 153 policy.mPasswordHistory = getValueInt(); 154 break; 155 case Tags.PROVISION_ALLOW_CAMERA: 156 // We need to check to see if it's possible to disable the camera. It's not 157 // possible on devices with managed profiles. 158 policy.mDontAllowCamera = (getValueInt() == 0); 159 if (policy.mDontAllowCamera) { 160 // See if it's possible to disable the camera. 161 if (!PolicyServiceProxy.canDisableCamera(mContext)) { 162 tagIsSupported = false; 163 policy.mDontAllowCamera = false; 164 } 165 } 166 break; 167 case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD: 168 // Ignore this unless there's any MSFT documentation for what this means 169 // Hint: I haven't seen any that's more specific than "simple" 170 getValue(); 171 break; 172 // The following policies, if false, can't be supported at the moment 173 case Tags.PROVISION_ALLOW_STORAGE_CARD: 174 case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS: 175 case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES: 176 case Tags.PROVISION_ALLOW_WIFI: 177 case Tags.PROVISION_ALLOW_TEXT_MESSAGING: 178 case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL: 179 case Tags.PROVISION_ALLOW_IRDA: 180 case Tags.PROVISION_ALLOW_HTML_EMAIL: 181 case Tags.PROVISION_ALLOW_BROWSER: 182 case Tags.PROVISION_ALLOW_CONSUMER_EMAIL: 183 case Tags.PROVISION_ALLOW_INTERNET_SHARING: 184 if (getValueInt() == 0) { 185 tagIsSupported = false; 186 switch(tag) { 187 case Tags.PROVISION_ALLOW_STORAGE_CARD: 188 res = R.string.policy_dont_allow_storage_cards; 189 break; 190 case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS: 191 res = R.string.policy_dont_allow_unsigned_apps; 192 break; 193 case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES: 194 res = R.string.policy_dont_allow_unsigned_installers; 195 break; 196 case Tags.PROVISION_ALLOW_WIFI: 197 res = R.string.policy_dont_allow_wifi; 198 break; 199 case Tags.PROVISION_ALLOW_TEXT_MESSAGING: 200 res = R.string.policy_dont_allow_text_messaging; 201 break; 202 case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL: 203 res = R.string.policy_dont_allow_pop_imap; 204 break; 205 case Tags.PROVISION_ALLOW_IRDA: 206 res = R.string.policy_dont_allow_irda; 207 break; 208 case Tags.PROVISION_ALLOW_HTML_EMAIL: 209 res = R.string.policy_dont_allow_html; 210 policy.mDontAllowHtml = true; 211 break; 212 case Tags.PROVISION_ALLOW_BROWSER: 213 res = R.string.policy_dont_allow_browser; 214 break; 215 case Tags.PROVISION_ALLOW_CONSUMER_EMAIL: 216 res = R.string.policy_dont_allow_consumer_email; 217 break; 218 case Tags.PROVISION_ALLOW_INTERNET_SHARING: 219 res = R.string.policy_dont_allow_internet_sharing; 220 break; 221 } 222 if (res > 0) { 223 unsupportedList.add(res); 224 } 225 } 226 break; 227 case Tags.PROVISION_ATTACHMENTS_ENABLED: 228 policy.mDontAllowAttachments = getValueInt() != 1; 229 break; 230 // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed 231 case Tags.PROVISION_ALLOW_BLUETOOTH: 232 if (getValueInt() != 2) { 233 tagIsSupported = false; 234 unsupportedList.add(R.string.policy_bluetooth_restricted); 235 } 236 break; 237 // We may now support device (internal) encryption; we'll check this capability 238 // below with the call to SecurityPolicy.isSupported() 239 case Tags.PROVISION_REQUIRE_DEVICE_ENCRYPTION: 240 if (getValueInt() == 1) { 241 if (!deviceSupportsEncryption()) { 242 tagIsSupported = false; 243 unsupportedList.add(R.string.policy_require_encryption); 244 } else { 245 policy.mRequireEncryption = true; 246 } 247 } 248 break; 249 // Note that DEVICE_ENCRYPTION_ENABLED refers to SD card encryption, which the OS 250 // does not yet support. 251 case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED: 252 if (getValueInt() == 1) { 253 log("Policy requires SD card encryption"); 254 // Let's see if this can be supported on our device... 255 if (deviceSupportsEncryption()) { 256 // NOTE: Private API! 257 // Go through volumes; if ANY are removable, we can't support this 258 // policy. 259 tagIsSupported = !hasRemovableStorage(); 260 if (tagIsSupported) { 261 // If this policy is requested, we MUST also require encryption 262 log("Device supports SD card encryption"); 263 policy.mRequireEncryption = true; 264 break; 265 } 266 } else { 267 log("Device doesn't support encryption; failing"); 268 tagIsSupported = false; 269 } 270 // If we fall through, we can't support the policy 271 unsupportedList.add(R.string.policy_require_sd_encryption); 272 } 273 break; 274 // Note this policy; we enforce it in EasService 275 case Tags.PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING: 276 policy.mRequireManualSyncWhenRoaming = getValueInt() == 1; 277 break; 278 // We are allowed to accept policies, regardless of value of this tag 279 // TODO: When we DO support a recovery password, we need to store the value in 280 // the account (so we know to utilize it) 281 case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED: 282 // Read, but ignore, value 283 policy.mPasswordRecoveryEnabled = getValueInt() == 1; 284 break; 285 // The following policies, if true, can't be supported at the moment 286 case Tags.PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES: 287 case Tags.PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES: 288 case Tags.PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM: 289 case Tags.PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM: 290 if (getValueInt() == 1) { 291 tagIsSupported = false; 292 if (!smimeRequired) { 293 unsupportedList.add(R.string.policy_require_smime); 294 smimeRequired = true; 295 } 296 } 297 break; 298 case Tags.PROVISION_MAX_ATTACHMENT_SIZE: 299 int max = getValueInt(); 300 if (max > 0) { 301 policy.mMaxAttachmentSize = max; 302 } 303 break; 304 // Complex characters are supported 305 case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS: 306 policy.mPasswordComplexChars = getValueInt(); 307 break; 308 // The following policies are moot; they allow functionality that we don't support 309 case Tags.PROVISION_ALLOW_DESKTOP_SYNC: 310 case Tags.PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION: 311 case Tags.PROVISION_ALLOW_SMIME_SOFT_CERTS: 312 case Tags.PROVISION_ALLOW_REMOTE_DESKTOP: 313 skipTag(); 314 break; 315 // We don't handle approved/unapproved application lists 316 case Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST: 317 case Tags.PROVISION_APPROVED_APPLICATION_LIST: 318 // Parse and throw away the content 319 if (specifiesApplications(tag)) { 320 tagIsSupported = false; 321 if (tag == Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST) { 322 unsupportedList.add(R.string.policy_app_blacklist); 323 } else { 324 unsupportedList.add(R.string.policy_app_whitelist); 325 } 326 } 327 break; 328 // We accept calendar age, since we never ask for more than two weeks, and that's 329 // the most restrictive policy 330 case Tags.PROVISION_MAX_CALENDAR_AGE_FILTER: 331 policy.mMaxCalendarLookback = getValueInt(); 332 break; 333 // We handle max email lookback 334 case Tags.PROVISION_MAX_EMAIL_AGE_FILTER: 335 policy.mMaxEmailLookback = getValueInt(); 336 break; 337 // We currently reject these next two policies 338 case Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE: 339 case Tags.PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE: 340 String value = getValue(); 341 // -1 indicates no required truncation 342 if (!value.equals("-1")) { 343 max = Integer.parseInt(value); 344 if (tag == Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE) { 345 policy.mMaxTextTruncationSize = max; 346 unsupportedList.add(R.string.policy_text_truncation); 347 } else { 348 policy.mMaxHtmlTruncationSize = max; 349 unsupportedList.add(R.string.policy_html_truncation); 350 } 351 tagIsSupported = false; 352 } 353 break; 354 default: 355 skipTag(); 356 } 357 358 if (!tagIsSupported) { 359 log("Policy not supported: " + tag); 360 mIsSupportable = false; 361 } 362 } 363 364 // Make sure policy settings are valid; password not enabled trumps other password settings 365 if (!passwordEnabled) { 366 policy.mPasswordMode = Policy.PASSWORD_MODE_NONE; 367 } 368 369 if (!unsupportedList.isEmpty()) { 370 StringBuilder sb = new StringBuilder(); 371 for (int res: unsupportedList) { 372 addPolicyString(sb, res); 373 } 374 policy.mProtocolPoliciesUnsupported = sb.toString(); 375 } 376 377 setPolicy(policy); 378 } 379 380 /** 381 * Return whether or not either of the application list tags specifies any applications 382 * @param endTag the tag whose children we're walking through 383 * @return whether any applications were specified (by name or by hash) 384 * @throws IOException 385 */ specifiesApplications(int endTag)386 private boolean specifiesApplications(int endTag) throws IOException { 387 boolean specifiesApplications = false; 388 while (nextTag(endTag) != END) { 389 switch (tag) { 390 case Tags.PROVISION_APPLICATION_NAME: 391 case Tags.PROVISION_HASH: 392 specifiesApplications = true; 393 break; 394 default: 395 skipTag(); 396 } 397 } 398 return specifiesApplications; 399 } 400 parseProvisionDocXml(String doc)401 /*package*/ void parseProvisionDocXml(String doc) throws IOException { 402 Policy policy = new Policy(); 403 404 try { 405 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 406 XmlPullParser parser = factory.newPullParser(); 407 parser.setInput(new ByteArrayInputStream(doc.getBytes()), "UTF-8"); 408 int type = parser.getEventType(); 409 if (type == XmlPullParser.START_DOCUMENT) { 410 type = parser.next(); 411 if (type == XmlPullParser.START_TAG) { 412 String tagName = parser.getName(); 413 if (tagName.equals("wap-provisioningdoc")) { 414 parseWapProvisioningDoc(parser, policy); 415 } 416 } 417 } 418 } catch (XmlPullParserException e) { 419 throw new IOException(); 420 } 421 422 setPolicy(policy); 423 } 424 425 /** 426 * Return true if password is required; otherwise false. 427 */ parseSecurityPolicy(XmlPullParser parser)428 private static boolean parseSecurityPolicy(XmlPullParser parser) 429 throws XmlPullParserException, IOException { 430 boolean passwordRequired = true; 431 while (true) { 432 int type = parser.nextTag(); 433 if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) { 434 break; 435 } else if (type == XmlPullParser.START_TAG) { 436 String tagName = parser.getName(); 437 if (tagName.equals("parm")) { 438 String name = parser.getAttributeValue(null, "name"); 439 if (name.equals("4131")) { 440 String value = parser.getAttributeValue(null, "value"); 441 if (value.equals("1")) { 442 passwordRequired = false; 443 } 444 } 445 } 446 } 447 } 448 return passwordRequired; 449 } 450 parseCharacteristic(XmlPullParser parser, Policy policy)451 private static void parseCharacteristic(XmlPullParser parser, Policy policy) 452 throws XmlPullParserException, IOException { 453 boolean enforceInactivityTimer = true; 454 while (true) { 455 int type = parser.nextTag(); 456 if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) { 457 break; 458 } else if (type == XmlPullParser.START_TAG) { 459 if (parser.getName().equals("parm")) { 460 String name = parser.getAttributeValue(null, "name"); 461 String value = parser.getAttributeValue(null, "value"); 462 if (name.equals("AEFrequencyValue")) { 463 if (enforceInactivityTimer) { 464 if (value.equals("0")) { 465 policy.mMaxScreenLockTime = 1; 466 } else { 467 policy.mMaxScreenLockTime = 60*Integer.parseInt(value); 468 } 469 } 470 } else if (name.equals("AEFrequencyType")) { 471 // "0" here means we don't enforce an inactivity timeout 472 if (value.equals("0")) { 473 enforceInactivityTimer = false; 474 } 475 } else if (name.equals("DeviceWipeThreshold")) { 476 policy.mPasswordMaxFails = Integer.parseInt(value); 477 } else if (name.equals("CodewordFrequency")) { 478 // Ignore; has no meaning for us 479 } else if (name.equals("MinimumPasswordLength")) { 480 policy.mPasswordMinLength = Integer.parseInt(value); 481 } else if (name.equals("PasswordComplexity")) { 482 if (value.equals("0")) { 483 policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG; 484 } else { 485 policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE; 486 } 487 } 488 } 489 } 490 } 491 } 492 parseRegistry(XmlPullParser parser, Policy policy)493 private static void parseRegistry(XmlPullParser parser, Policy policy) 494 throws XmlPullParserException, IOException { 495 while (true) { 496 int type = parser.nextTag(); 497 if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) { 498 break; 499 } else if (type == XmlPullParser.START_TAG) { 500 String name = parser.getName(); 501 if (name.equals("characteristic")) { 502 parseCharacteristic(parser, policy); 503 } 504 } 505 } 506 } 507 parseWapProvisioningDoc(XmlPullParser parser, Policy policy)508 private static void parseWapProvisioningDoc(XmlPullParser parser, Policy policy) 509 throws XmlPullParserException, IOException { 510 while (true) { 511 int type = parser.nextTag(); 512 if (type == XmlPullParser.END_TAG && parser.getName().equals("wap-provisioningdoc")) { 513 break; 514 } else if (type == XmlPullParser.START_TAG) { 515 String name = parser.getName(); 516 if (name.equals("characteristic")) { 517 String atype = parser.getAttributeValue(null, "type"); 518 if (atype.equals("SecurityPolicy")) { 519 // If a password isn't required, stop here 520 if (!parseSecurityPolicy(parser)) { 521 return; 522 } 523 } else if (atype.equals("Registry")) { 524 parseRegistry(parser, policy); 525 return; 526 } 527 } 528 } 529 } 530 } 531 parseProvisionData()532 private void parseProvisionData() throws IOException { 533 while (nextTag(Tags.PROVISION_DATA) != END) { 534 if (tag == Tags.PROVISION_EAS_PROVISION_DOC) { 535 parseProvisionDocWbxml(); 536 } else { 537 skipTag(); 538 } 539 } 540 } 541 parsePolicy()542 private void parsePolicy() throws IOException { 543 String policyType = null; 544 while (nextTag(Tags.PROVISION_POLICY) != END) { 545 switch (tag) { 546 case Tags.PROVISION_POLICY_TYPE: 547 policyType = getValue(); 548 LogUtils.d(TAG, "Policy type: %s", policyType); 549 break; 550 case Tags.PROVISION_POLICY_KEY: 551 mSecuritySyncKey = getValue(); 552 break; 553 case Tags.PROVISION_STATUS: 554 LogUtils.d(TAG, "Policy status: %s", getValue()); 555 break; 556 case Tags.PROVISION_DATA: 557 if (policyType.equalsIgnoreCase(EasProvision.EAS_2_POLICY_TYPE)) { 558 // Parse the old style XML document 559 parseProvisionDocXml(getValue()); 560 } else { 561 // Parse the newer WBXML data 562 parseProvisionData(); 563 } 564 break; 565 default: 566 skipTag(); 567 } 568 } 569 } 570 parsePolicies()571 private void parsePolicies() throws IOException { 572 while (nextTag(Tags.PROVISION_POLICIES) != END) { 573 if (tag == Tags.PROVISION_POLICY) { 574 parsePolicy(); 575 } else { 576 skipTag(); 577 } 578 } 579 } 580 parseDeviceInformation()581 private void parseDeviceInformation() throws IOException { 582 while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) { 583 if (tag == Tags.SETTINGS_STATUS) { 584 LogUtils.d(TAG, "DeviceInformation status: %s", getValue()); 585 } else { 586 skipTag(); 587 } 588 } 589 } 590 591 @Override parse()592 public boolean parse() throws IOException { 593 boolean res = false; 594 if (nextTag(START_DOCUMENT) != Tags.PROVISION_PROVISION) { 595 throw new IOException(); 596 } 597 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 598 switch (tag) { 599 case Tags.PROVISION_STATUS: 600 int status = getValueInt(); 601 LogUtils.d(TAG, "Provision status: %d", status); 602 res = (status == 1); 603 break; 604 case Tags.SETTINGS_DEVICE_INFORMATION: 605 parseDeviceInformation(); 606 break; 607 case Tags.PROVISION_POLICIES: 608 parsePolicies(); 609 break; 610 case Tags.PROVISION_REMOTE_WIPE: 611 // Indicate remote wipe command received 612 mRemoteWipe = true; 613 break; 614 default: 615 skipTag(); 616 } 617 } 618 return res; 619 } 620 621 /** 622 * In order to determine whether the device has removable storage, we need to use the 623 * StorageVolume class, which is hidden (for now) by the framework. Without this, we'd have 624 * to reject all policies that require sd card encryption. 625 * 626 * TODO: Rewrite this when an appropriate API is available from the framework 627 */ hasRemovableStorage()628 private boolean hasRemovableStorage() { 629 final File[] cacheDirs = ContextCompat.getExternalCacheDirs(mContext); 630 return Environment.isExternalStorageRemovable() 631 || (cacheDirs != null && cacheDirs.length > 1); 632 } 633 } 634