com.android.exchange.adapter.ProvisionParser.java Source code

Java tutorial

Introduction

Here is the source code for com.android.exchange.adapter.ProvisionParser.java

Source

/* Copyright (C) 2010 The Android Open Source Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.exchange.adapter;

import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.res.Resources;
import android.os.Environment;
import android.support.v4.content.ContextCompat;

import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.service.PolicyServiceProxy;
import com.android.exchange.Eas;
import com.android.exchange.R;
import com.android.exchange.eas.EasProvision;
import com.android.mail.utils.LogUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

/**
 * Parse the result of the Provision command
 */
public class ProvisionParser extends Parser {
    private static final String TAG = Eas.LOG_TAG;

    private final Context mContext;
    private Policy mPolicy = null;
    private String mSecuritySyncKey = null;
    private boolean mRemoteWipe = false;
    private boolean mIsSupportable = true;
    private boolean smimeRequired = false;
    private final Resources mResources;

    public ProvisionParser(final Context context, final InputStream in) throws IOException {
        super(in);
        mContext = context;
        mResources = context.getResources();
    }

    public Policy getPolicy() {
        return mPolicy;
    }

    public String getSecuritySyncKey() {
        return mSecuritySyncKey;
    }

    public void setSecuritySyncKey(String securitySyncKey) {
        mSecuritySyncKey = securitySyncKey;
    }

    public boolean getRemoteWipe() {
        return mRemoteWipe;
    }

    public boolean hasSupportablePolicySet() {
        return (mPolicy != null) && mIsSupportable;
    }

    public void clearUnsupportablePolicies() {
        mIsSupportable = true;
        mPolicy.mProtocolPoliciesUnsupported = null;
    }

    private void addPolicyString(StringBuilder sb, int res) {
        sb.append(mResources.getString(res));
        sb.append(Policy.POLICY_STRING_DELIMITER);
    }

    /**
     * Complete setup of a Policy; we normalize it first (removing inconsistencies, etc.) and then
     * generate the tokenized "protocol policies enforced" string.  Note that unsupported policies
     * must have been added prior to calling this method (this is only a possibility with wbxml
     * policy documents, as all versions of the OS support the policies in xml documents).
     */
    private void setPolicy(Policy policy) {
        policy.normalize();
        StringBuilder sb = new StringBuilder();
        if (policy.mDontAllowAttachments) {
            addPolicyString(sb, R.string.policy_dont_allow_attachments);
        }
        if (policy.mRequireManualSyncWhenRoaming) {
            addPolicyString(sb, R.string.policy_require_manual_sync_roaming);
        }
        policy.mProtocolPoliciesEnforced = sb.toString();
        mPolicy = policy;
    }

    private boolean deviceSupportsEncryption() {
        DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
        int status = dpm.getStorageEncryptionStatus();
        return status != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED;
    }

    private void parseProvisionDocWbxml() throws IOException {
        Policy policy = new Policy();
        ArrayList<Integer> unsupportedList = new ArrayList<Integer>();
        boolean passwordEnabled = false;

        while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
            boolean tagIsSupported = true;
            int res = 0;
            switch (tag) {
            case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
                if (getValueInt() == 1) {
                    passwordEnabled = true;
                    if (policy.mPasswordMode == Policy.PASSWORD_MODE_NONE) {
                        policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE;
                    }
                }
                break;
            case Tags.PROVISION_MIN_DEVICE_PASSWORD_LENGTH:
                policy.mPasswordMinLength = getValueInt();
                break;
            case Tags.PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED:
                if (getValueInt() == 1) {
                    policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG;
                }
                break;
            case Tags.PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK:
                // EAS gives us seconds, which is, happily, what the PolicySet requires
                policy.mMaxScreenLockTime = getValueInt();
                break;
            case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
                policy.mPasswordMaxFails = getValueInt();
                break;
            case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
                policy.mPasswordExpirationDays = getValueInt();
                break;
            case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
                policy.mPasswordHistory = getValueInt();
                break;
            case Tags.PROVISION_ALLOW_CAMERA:
                // We need to check to see if it's possible to disable the camera. It's not
                // possible on devices with managed profiles.
                policy.mDontAllowCamera = (getValueInt() == 0);
                if (policy.mDontAllowCamera) {
                    // See if it's possible to disable the camera.
                    if (!PolicyServiceProxy.canDisableCamera(mContext)) {
                        tagIsSupported = false;
                        policy.mDontAllowCamera = false;
                    }
                }
                break;
            case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD:
                // Ignore this unless there's any MSFT documentation for what this means
                // Hint: I haven't seen any that's more specific than "simple"
                getValue();
                break;
            // The following policies, if false, can't be supported at the moment
            case Tags.PROVISION_ALLOW_STORAGE_CARD:
            case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS:
            case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES:
            case Tags.PROVISION_ALLOW_WIFI:
            case Tags.PROVISION_ALLOW_TEXT_MESSAGING:
            case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL:
            case Tags.PROVISION_ALLOW_IRDA:
            case Tags.PROVISION_ALLOW_HTML_EMAIL:
            case Tags.PROVISION_ALLOW_BROWSER:
            case Tags.PROVISION_ALLOW_CONSUMER_EMAIL:
            case Tags.PROVISION_ALLOW_INTERNET_SHARING:
                if (getValueInt() == 0) {
                    tagIsSupported = false;
                    switch (tag) {
                    case Tags.PROVISION_ALLOW_STORAGE_CARD:
                        res = R.string.policy_dont_allow_storage_cards;
                        break;
                    case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS:
                        res = R.string.policy_dont_allow_unsigned_apps;
                        break;
                    case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES:
                        res = R.string.policy_dont_allow_unsigned_installers;
                        break;
                    case Tags.PROVISION_ALLOW_WIFI:
                        res = R.string.policy_dont_allow_wifi;
                        break;
                    case Tags.PROVISION_ALLOW_TEXT_MESSAGING:
                        res = R.string.policy_dont_allow_text_messaging;
                        break;
                    case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL:
                        res = R.string.policy_dont_allow_pop_imap;
                        break;
                    case Tags.PROVISION_ALLOW_IRDA:
                        res = R.string.policy_dont_allow_irda;
                        break;
                    case Tags.PROVISION_ALLOW_HTML_EMAIL:
                        res = R.string.policy_dont_allow_html;
                        policy.mDontAllowHtml = true;
                        break;
                    case Tags.PROVISION_ALLOW_BROWSER:
                        res = R.string.policy_dont_allow_browser;
                        break;
                    case Tags.PROVISION_ALLOW_CONSUMER_EMAIL:
                        res = R.string.policy_dont_allow_consumer_email;
                        break;
                    case Tags.PROVISION_ALLOW_INTERNET_SHARING:
                        res = R.string.policy_dont_allow_internet_sharing;
                        break;
                    }
                    if (res > 0) {
                        unsupportedList.add(res);
                    }
                }
                break;
            case Tags.PROVISION_ATTACHMENTS_ENABLED:
                policy.mDontAllowAttachments = getValueInt() != 1;
                break;
            // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed
            case Tags.PROVISION_ALLOW_BLUETOOTH:
                if (getValueInt() != 2) {
                    tagIsSupported = false;
                    unsupportedList.add(R.string.policy_bluetooth_restricted);
                }
                break;
            // We may now support device (internal) encryption; we'll check this capability
            // below with the call to SecurityPolicy.isSupported()
            case Tags.PROVISION_REQUIRE_DEVICE_ENCRYPTION:
                if (getValueInt() == 1) {
                    if (!deviceSupportsEncryption()) {
                        tagIsSupported = false;
                        unsupportedList.add(R.string.policy_require_encryption);
                    } else {
                        policy.mRequireEncryption = true;
                    }
                }
                break;
            // Note that DEVICE_ENCRYPTION_ENABLED refers to SD card encryption, which the OS
            // does not yet support.
            case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED:
                if (getValueInt() == 1) {
                    log("Policy requires SD card encryption");
                    // Let's see if this can be supported on our device...
                    if (deviceSupportsEncryption()) {
                        // NOTE: Private API!
                        // Go through volumes; if ANY are removable, we can't support this
                        // policy.
                        tagIsSupported = !hasRemovableStorage();
                        if (tagIsSupported) {
                            // If this policy is requested, we MUST also require encryption
                            log("Device supports SD card encryption");
                            policy.mRequireEncryption = true;
                            break;
                        }
                    } else {
                        log("Device doesn't support encryption; failing");
                        tagIsSupported = false;
                    }
                    // If we fall through, we can't support the policy
                    unsupportedList.add(R.string.policy_require_sd_encryption);
                }
                break;
            // Note this policy; we enforce it in EasService
            case Tags.PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING:
                policy.mRequireManualSyncWhenRoaming = getValueInt() == 1;
                break;
            // We are allowed to accept policies, regardless of value of this tag
            // TODO: When we DO support a recovery password, we need to store the value in
            // the account (so we know to utilize it)
            case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED:
                // Read, but ignore, value
                policy.mPasswordRecoveryEnabled = getValueInt() == 1;
                break;
            // The following policies, if true, can't be supported at the moment
            case Tags.PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES:
            case Tags.PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES:
            case Tags.PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM:
            case Tags.PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM:
                if (getValueInt() == 1) {
                    tagIsSupported = false;
                    if (!smimeRequired) {
                        unsupportedList.add(R.string.policy_require_smime);
                        smimeRequired = true;
                    }
                }
                break;
            case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
                int max = getValueInt();
                if (max > 0) {
                    policy.mMaxAttachmentSize = max;
                }
                break;
            // Complex characters are supported
            case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS:
                policy.mPasswordComplexChars = getValueInt();
                break;
            // The following policies are moot; they allow functionality that we don't support
            case Tags.PROVISION_ALLOW_DESKTOP_SYNC:
            case Tags.PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION:
            case Tags.PROVISION_ALLOW_SMIME_SOFT_CERTS:
            case Tags.PROVISION_ALLOW_REMOTE_DESKTOP:
                skipTag();
                break;
            // We don't handle approved/unapproved application lists
            case Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST:
            case Tags.PROVISION_APPROVED_APPLICATION_LIST:
                // Parse and throw away the content
                if (specifiesApplications(tag)) {
                    tagIsSupported = false;
                    if (tag == Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST) {
                        unsupportedList.add(R.string.policy_app_blacklist);
                    } else {
                        unsupportedList.add(R.string.policy_app_whitelist);
                    }
                }
                break;
            // We accept calendar age, since we never ask for more than two weeks, and that's
            // the most restrictive policy
            case Tags.PROVISION_MAX_CALENDAR_AGE_FILTER:
                policy.mMaxCalendarLookback = getValueInt();
                break;
            // We handle max email lookback
            case Tags.PROVISION_MAX_EMAIL_AGE_FILTER:
                policy.mMaxEmailLookback = getValueInt();
                break;
            // We currently reject these next two policies
            case Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE:
            case Tags.PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE:
                String value = getValue();
                // -1 indicates no required truncation
                if (!value.equals("-1")) {
                    max = Integer.parseInt(value);
                    if (tag == Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE) {
                        policy.mMaxTextTruncationSize = max;
                        unsupportedList.add(R.string.policy_text_truncation);
                    } else {
                        policy.mMaxHtmlTruncationSize = max;
                        unsupportedList.add(R.string.policy_html_truncation);
                    }
                    tagIsSupported = false;
                }
                break;
            default:
                skipTag();
            }

            if (!tagIsSupported) {
                log("Policy not supported: " + tag);
                mIsSupportable = false;
            }
        }

        // Make sure policy settings are valid; password not enabled trumps other password settings
        if (!passwordEnabled) {
            policy.mPasswordMode = Policy.PASSWORD_MODE_NONE;
        }

        if (!unsupportedList.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (int res : unsupportedList) {
                addPolicyString(sb, res);
            }
            policy.mProtocolPoliciesUnsupported = sb.toString();
        }

        setPolicy(policy);
    }

    /**
     * Return whether or not either of the application list tags specifies any applications
     * @param endTag the tag whose children we're walking through
     * @return whether any applications were specified (by name or by hash)
     * @throws IOException
     */
    private boolean specifiesApplications(int endTag) throws IOException {
        boolean specifiesApplications = false;
        while (nextTag(endTag) != END) {
            switch (tag) {
            case Tags.PROVISION_APPLICATION_NAME:
            case Tags.PROVISION_HASH:
                specifiesApplications = true;
                break;
            default:
                skipTag();
            }
        }
        return specifiesApplications;
    }

    /*package*/ void parseProvisionDocXml(String doc) throws IOException {
        Policy policy = new Policy();

        try {
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser parser = factory.newPullParser();
            parser.setInput(new ByteArrayInputStream(doc.getBytes()), "UTF-8");
            int type = parser.getEventType();
            if (type == XmlPullParser.START_DOCUMENT) {
                type = parser.next();
                if (type == XmlPullParser.START_TAG) {
                    String tagName = parser.getName();
                    if (tagName.equals("wap-provisioningdoc")) {
                        parseWapProvisioningDoc(parser, policy);
                    }
                }
            }
        } catch (XmlPullParserException e) {
            throw new IOException();
        }

        setPolicy(policy);
    }

    /**
     * Return true if password is required; otherwise false.
     */
    private static boolean parseSecurityPolicy(XmlPullParser parser) throws XmlPullParserException, IOException {
        boolean passwordRequired = true;
        while (true) {
            int type = parser.nextTag();
            if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                String tagName = parser.getName();
                if (tagName.equals("parm")) {
                    String name = parser.getAttributeValue(null, "name");
                    if (name.equals("4131")) {
                        String value = parser.getAttributeValue(null, "value");
                        if (value.equals("1")) {
                            passwordRequired = false;
                        }
                    }
                }
            }
        }
        return passwordRequired;
    }

    private static void parseCharacteristic(XmlPullParser parser, Policy policy)
            throws XmlPullParserException, IOException {
        boolean enforceInactivityTimer = true;
        while (true) {
            int type = parser.nextTag();
            if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                if (parser.getName().equals("parm")) {
                    String name = parser.getAttributeValue(null, "name");
                    String value = parser.getAttributeValue(null, "value");
                    if (name.equals("AEFrequencyValue")) {
                        if (enforceInactivityTimer) {
                            if (value.equals("0")) {
                                policy.mMaxScreenLockTime = 1;
                            } else {
                                policy.mMaxScreenLockTime = 60 * Integer.parseInt(value);
                            }
                        }
                    } else if (name.equals("AEFrequencyType")) {
                        // "0" here means we don't enforce an inactivity timeout
                        if (value.equals("0")) {
                            enforceInactivityTimer = false;
                        }
                    } else if (name.equals("DeviceWipeThreshold")) {
                        policy.mPasswordMaxFails = Integer.parseInt(value);
                    } else if (name.equals("CodewordFrequency")) {
                        // Ignore; has no meaning for us
                    } else if (name.equals("MinimumPasswordLength")) {
                        policy.mPasswordMinLength = Integer.parseInt(value);
                    } else if (name.equals("PasswordComplexity")) {
                        if (value.equals("0")) {
                            policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG;
                        } else {
                            policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE;
                        }
                    }
                }
            }
        }
    }

    private static void parseRegistry(XmlPullParser parser, Policy policy)
            throws XmlPullParserException, IOException {
        while (true) {
            int type = parser.nextTag();
            if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                String name = parser.getName();
                if (name.equals("characteristic")) {
                    parseCharacteristic(parser, policy);
                }
            }
        }
    }

    private static void parseWapProvisioningDoc(XmlPullParser parser, Policy policy)
            throws XmlPullParserException, IOException {
        while (true) {
            int type = parser.nextTag();
            if (type == XmlPullParser.END_TAG && parser.getName().equals("wap-provisioningdoc")) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                String name = parser.getName();
                if (name.equals("characteristic")) {
                    String atype = parser.getAttributeValue(null, "type");
                    if (atype.equals("SecurityPolicy")) {
                        // If a password isn't required, stop here
                        if (!parseSecurityPolicy(parser)) {
                            return;
                        }
                    } else if (atype.equals("Registry")) {
                        parseRegistry(parser, policy);
                        return;
                    }
                }
            }
        }
    }

    private void parseProvisionData() throws IOException {
        while (nextTag(Tags.PROVISION_DATA) != END) {
            if (tag == Tags.PROVISION_EAS_PROVISION_DOC) {
                parseProvisionDocWbxml();
            } else {
                skipTag();
            }
        }
    }

    private void parsePolicy() throws IOException {
        String policyType = null;
        while (nextTag(Tags.PROVISION_POLICY) != END) {
            switch (tag) {
            case Tags.PROVISION_POLICY_TYPE:
                policyType = getValue();
                LogUtils.d(TAG, "Policy type: %s", policyType);
                break;
            case Tags.PROVISION_POLICY_KEY:
                mSecuritySyncKey = getValue();
                break;
            case Tags.PROVISION_STATUS:
                LogUtils.d(TAG, "Policy status: %s", getValue());
                break;
            case Tags.PROVISION_DATA:
                if (policyType.equalsIgnoreCase(EasProvision.EAS_2_POLICY_TYPE)) {
                    // Parse the old style XML document
                    parseProvisionDocXml(getValue());
                } else {
                    // Parse the newer WBXML data
                    parseProvisionData();
                }
                break;
            default:
                skipTag();
            }
        }
    }

    private void parsePolicies() throws IOException {
        while (nextTag(Tags.PROVISION_POLICIES) != END) {
            if (tag == Tags.PROVISION_POLICY) {
                parsePolicy();
            } else {
                skipTag();
            }
        }
    }

    private void parseDeviceInformation() throws IOException {
        while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) {
            if (tag == Tags.SETTINGS_STATUS) {
                LogUtils.d(TAG, "DeviceInformation status: %s", getValue());
            } else {
                skipTag();
            }
        }
    }

    @Override
    public boolean parse() throws IOException {
        boolean res = false;
        if (nextTag(START_DOCUMENT) != Tags.PROVISION_PROVISION) {
            throw new IOException();
        }
        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
            switch (tag) {
            case Tags.PROVISION_STATUS:
                int status = getValueInt();
                LogUtils.d(TAG, "Provision status: %d", status);
                res = (status == 1);
                break;
            case Tags.SETTINGS_DEVICE_INFORMATION:
                parseDeviceInformation();
                break;
            case Tags.PROVISION_POLICIES:
                parsePolicies();
                break;
            case Tags.PROVISION_REMOTE_WIPE:
                // Indicate remote wipe command received
                mRemoteWipe = true;
                break;
            default:
                skipTag();
            }
        }
        return res;
    }

    /**
     * In order to determine whether the device has removable storage, we need to use the
     * StorageVolume class, which is hidden (for now) by the framework.  Without this, we'd have
     * to reject all policies that require sd card encryption.
     *
     * TODO: Rewrite this when an appropriate API is available from the framework
     */
    private boolean hasRemovableStorage() {
        final File[] cacheDirs = ContextCompat.getExternalCacheDirs(mContext);
        return Environment.isExternalStorageRemovable() || (cacheDirs != null && cacheDirs.length > 1);
    }
}