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