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