com.ifeng.util.download.DownloadThread.java Source code

Java tutorial

Introduction

Here is the source code for com.ifeng.util.download.DownloadThread.java

Source

/*
 * Copyright (C) 2008 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.ifeng.util.download;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SyncFailedException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;

import android.content.ContentValues;
import android.content.Context;
import android.os.Handler;
import android.os.PowerManager;
import android.os.Process;
import android.text.TextUtils;
import android.widget.Toast;

import com.ifeng.android.R;
import com.ifeng.util.AppUtils;
import com.ifeng.util.logging.Log;
import com.ifeng.util.logging.Utils;
import com.ifeng.util.net.ProxyHttpClient;

/**
 * Runs an actual download
 */
public class DownloadThread extends Thread {

    /** context. */
    private final Context mContext;
    /** download info. */
    private final DownloadInfo mInfo;
    /** SystemFacade. */
    private final SystemFacade mSystemFacade;

    /**
     * DownloadThread constructor.
     * 
     * @param context
     *            context
     * @param systemFacade
     *            systemFacade
     * @param info
     *            DownloadInfo
     */
    public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info) {
        mContext = context;
        mSystemFacade = systemFacade;
        mInfo = info;
    }

    /**
     * Returns the user agent provided by the initiating app, or use the default
     * one
     * 
     * @return see the description.
     */
    private String userAgent() {
        String userAgent = mInfo.mUserAgent;

        if (userAgent == null) {
            userAgent = Constants.DEFAULT_USER_AGENT;
        }
        return userAgent;
    }

    /**
     * State for the entire run() method.
     */
    private static class State {
        /** file name. */
        public String mFilename;
        /** output stream. */
        public FileOutputStream mStream;
        /** mime type. */
        public String mMimeType;
        /** retry count. */
        public boolean mCountRetry = false;
        /** mRetryAfter */
        public int mRetryAfter = 0;
        /** mRedirectCount */
        public int mRedirectCount = 0;
        /** mNewUri */
        public String mNewUri;
        /** mGotData */
        public boolean mGotData = false;
        /** mRequestUri */
        public String mRequestUri;

        /**
         * constructor.
         * 
         * @param info
         *            downloadinfo
         */
        public State(DownloadInfo info) {
            mMimeType = sanitizeMimeType(info.mMimeType);
            mRedirectCount = info.mRedirectCount;
            mRequestUri = info.mUri;
            mFilename = info.mFileName;
        }
    }

    /**
     * State within executeDownload()
     */
    private static class InnerState {
        /** current download size. */
        public int mBytesSoFar = 0;
        /** header etag. */
        public String mHeaderETag;
        /** mContinuingDownload. */
        public boolean mContinuingDownload = false;
        /** mHeaderContentLength. */
        public String mHeaderContentLength;
        /** mHeaderContentDisposition */
        public String mHeaderContentDisposition;
        /** mHeaderContentLocation */
        public String mHeaderContentLocation;
        /** mBytesNotified */
        public int mBytesNotified = 0;
        /** mTimeLastNotification */
        public long mTimeLastNotification = 0;
    }

    /**
     * Raised from methods called by run() to indicate that the current request
     * should be stopped immediately.
     * 
     * Note the message passed to this exception will be logged and therefore
     * must be guaranteed not to contain any PII, meaning it generally can't
     * include any information about the request URI, headers, or destination
     * filename.
     */
    private static class StopRequest extends Throwable {
        /** final status. */
        public int mFinalStatus;

        /**
         * stop he current request.
         * 
         * @param finalStatus
         *            finalStatus
         * @param message
         *            message
         */
        public StopRequest(int finalStatus, String message) {
            super(message);
            mFinalStatus = finalStatus;
        }

        /**
         * stop the current request.
         * 
         * @param finalStatus
         *            finalStatus
         * @param message
         *            message
         * @param throwable
         *            throwable
         */
        public StopRequest(int finalStatus, String message, Throwable throwable) {
            super(message, throwable);
            mFinalStatus = finalStatus;
        }
    }

    /**
     * Raised from methods called by executeDownload() to indicate that the
     * download should be retried immediately.
     */
    private static class RetryDownload extends Throwable {

    }

    /**
     * Executes the download in a separate thread
     */
    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        State state = new State(mInfo);
        ProxyHttpClient client = null;
        PowerManager.WakeLock wakeLock = null;
        int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;

        try {
            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
            wakeLock.acquire();

            if (Constants.LOGV) {
                Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
            }

            client = new ProxyHttpClient(mContext, userAgent());
            boolean finished = false;
            while (!finished) {
                Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
                HttpGet request = new HttpGet(state.mRequestUri.trim());
                try {
                    executeDownload(state, client, request);
                    finished = true;
                } catch (RetryDownload exc) {
                    // fall through
                    exc.printStackTrace();
                } finally {
                    request.abort();
                    request = null;
                }
            }

            if (Constants.LOGV) {
                Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
            }
            finalizeDestinationFile(state);
            finalStatus = Downloads.Impl.STATUS_SUCCESS;
        } catch (StopRequest error) {
            // remove the cause before printing, in case it contains PII
            Log.w(Constants.TAG, "Aborting request for download " + mInfo.mId + ": " + error.getMessage());
            Log.w(Constants.TAG, "download error:", error);
            finalStatus = error.mFinalStatus;
            displayMsg(finalStatus, error.getMessage());
            // 
            mInfo.mFailedReason = mInfo.mFailedReason + " |||| " + Utils.getStackTraceString(error);
            // fall through to finally block
        } catch (Throwable ex) { // sometimes the socket code throws unchecked
            // exceptions
            Log.w(Constants.TAG, "Exception for id " + mInfo.mId + ": " + ex);
            Log.w(Constants.TAG, "download error:", ex);
            finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
            displayMsg(finalStatus, ex.getMessage());
            // 
            mInfo.mFailedReason = mInfo.mFailedReason + "||||" + Utils.getStackTraceString(ex);
            // falls through to the code that reports an error
        } finally {
            if (wakeLock != null) {
                wakeLock.release();
                wakeLock = null;
            }
            if (client != null) {
                client.close();
                client = null;
            }
            cleanupDestination(state, finalStatus);

            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, state.mRedirectCount,
                    state.mGotData, state.mFilename, state.mNewUri, state.mMimeType);

            --DownloadService.mCurrentThreadNum;
            if (!Downloads.Impl.isStatusCompleted(finalStatus)) {
                mInfo.mHasActiveThread = false;
            }
        }
    }

    /**
     * ??
     * 
     * @param status
     *            ?? Downloads.status_*
     * @param msg
     *            ?
     */
    private void displayMsg(final int status, final String msg) {
        // ??
        // Intent intent = new Intent();
        // intent.setClass(mContext, DownloadMsgActivity.class);
        // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        // intent.putExtra(DownloadMsgActivity.EXTRA_STATUS, status);
        // intent.putExtra(DownloadMsgActivity.EXTRA_MSG, msg);
        //
        // mContext.startActivity(intent);
        // ????
        if (mInfo.mVisibility == Downloads.Impl.VISIBILITY_HIDDEN) {
            return;
        }
        Handler handler = new Handler(mContext.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                showMsg(status, msg);
            }
        });
    }

    /**
     * ?
     * 
     * @param status
     *            ?? Downloads.STATUS_XXX
     * @param msg
     *            ?
     */
    private void showMsg(int status, String msg) {
        String errorMsg = getErrorMessage(status);
        if (!TextUtils.isEmpty(errorMsg)) {
            Toast.makeText(mContext, errorMsg, Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * the appropriate error message for the failed download pointed to by
     * cursor.
     * 
     * @param status
     *            status ?? Downloads.STATUS_XXX
     * @return the appropriate error message for the failed download pointed to
     *         by cursor
     */
    private String getErrorMessage(int status) {
        switch (status) {

        case Downloads.STATUS_INSUFFICIENT_SPACE_ERROR:
            // if (isOnExternalStorage(cursor)) {
            return mContext.getString(R.string.dialog_insufficient_space_on_external);
        // } else {
        // return getString(R.string.dialog_insufficient_space_on_cache);
        // }

        case Downloads.STATUS_DEVICE_NOT_FOUND_ERROR:
            return mContext.getString(R.string.dialog_media_not_found);

        default:
            // return getString(R.string.dialog_failed_body);
            return null;
        }
    }

    /**
     * Fully execute a single download request - setup and send the request,
     * handle the response, and transfer the data to the destination file.
     * 
     * @param state
     *            state
     * @param client
     *            client
     * @param request
     *            request
     * @throws StopRequest
     *             StopRequest
     * @throws RetryDownload
     *             RetryDownload
     */
    private void executeDownload(State state, ProxyHttpClient client, HttpGet request)
            throws StopRequest, RetryDownload {
        InnerState innerState = new InnerState();

        byte[] data = new byte[Constants.BUFFER_SIZE];

        setupDestinationFile(state, innerState);
        addRequestHeaders(innerState, request);

        // check just before sending the request to avoid using an invalid
        // connection at all
        checkConnectivity(state);
        if (client.isWap()) {
            if (Constants.LOGV) {
                Log.d(Constants.TAG, "use wap download ");
            }
            wapDownload(state, innerState, client, request, data);
        } else {
            HttpResponse response = sendRequest(state, client, request);
            handleExceptionalStatus(state, innerState, response);
            if (Constants.LOGV) {
                Log.v(Constants.TAG, "received response for " + mInfo.mUri);
            }
            processResponseHeaders(state, innerState, response);
            InputStream entityStream = openResponseEntity(state, response);
            transferData(state, innerState, data, entityStream);
        }

    }

    /**
     * ?content-lengthContent-Range<br>
     * Content-Range: bytes 6758400-7065599/10899687
     * 
     * @param header
     *            header
     * @return content-length
     */
    private String parseContentRange(Header header) {
        String value = header.getValue();
        if (value != null && value.contains("/")) {
            value = value.substring(value.lastIndexOf("/") + 1);
        }
        return value;
    }

    /** wap */
    private static final int RANG_SIZE = 299 * 1024;

    /**
     * wap.?<br>
     * ??wap???300K,???10M.
     * wap? <br>
     * Http Range??Content-Range?????
     * ??1M({@link RANG_SIZE}).
     * 
     * @author liuqingbiao,zhangjunguo
     * @param state
     *            state
     * 
     * @param client
     *            client
     * @param request
     *            request
     * @throws StopRequest
     *             StopRequest
     * @throws RetryDownload
     *             RetryDownload
     * @param innerState
     *            InnerState
     * @param data
     *            buffer size
     */
    private void wapDownload(State state, InnerState innerState, ProxyHttpClient client, HttpGet request,
            byte[] data) throws StopRequest, RetryDownload {
        if (Constants.LOGV) {
            Log.d(Constants.TAG, "mInfo.CurrentBytes = " + mInfo.mCurrentBytes);
        }
        // ???innerState.mBytesSoFar.
        long begin = innerState.mBytesSoFar;
        long end = begin + RANG_SIZE;

        // Range headerRange??Header.
        request.removeHeaders("Range");
        // ?
        request.addHeader("Range", "bytes=" + begin + "-" + end);

        HttpResponse response = sendRequest(state, client, request);

        if (response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
            // Content-Range: bytes 6758400-7065599/10899687
            // ?header
            Header contentlength = response.getFirstHeader("Content-Range");
            if (contentlength != null) {
                String value = parseContentRange(contentlength);
                mInfo.mTotalBytes = Long.valueOf(value);
                innerState.mHeaderContentLength = value;
            } else {
                throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
                        "can't know size of download, giving up");
            }
        } else {
            // ??206Status
            handleExceptionalStatus(state, innerState, response);
        }

        if (Constants.LOGV) {
            Log.v(Constants.TAG, "received response for " + mInfo.mUri);
        }
        processResponseHeaders(state, innerState, response);
        // waptrue
        innerState.mContinuingDownload = true;
        InputStream entityStream = openResponseEntity(state, response);
        transferDataForWap(state, innerState, data, entityStream);

        begin = end + 1;
        end = end + RANG_SIZE;
        if (Constants.LOGV) {
            Log.d(Constants.TAG, "mBytesSoFar:" + innerState.mBytesSoFar);
            Log.d(Constants.TAG, "mInfo.mTotalBytes:" + mInfo.mTotalBytes);
        }
        // ???
        while (innerState.mBytesSoFar < mInfo.mTotalBytes) {
            if (Constants.LOGV) {
                Log.d(Constants.TAG, "?");
            }
            HttpGet waprequest = new HttpGet(state.mRequestUri);
            waprequest.addHeader("Range", "bytes=" + begin + "-" + end);

            response = sendRequest(state, client, waprequest);
            handleExceptionalStatus(state, innerState, response);
            processResponseHeadersForWap(state, innerState, response);
            entityStream = openResponseEntity(state, response);
            transferDataForWap(state, innerState, data, entityStream);

            begin = end + 1;
            end = end + RANG_SIZE;
            if (mInfo.mTotalBytes - innerState.mBytesSoFar < RANG_SIZE) {
                end = mInfo.mTotalBytes - 1; // waprange?-?range?
            }
            if (Constants.LOGV) {
                Log.d(Constants.TAG, "mBytesSoFar:" + innerState.mBytesSoFar);
            }
        }
    }

    /**
     * Transfer as much data as possible from the HTTP response to the
     * destination file.
     * 
     * @param state
     *            state.
     * @param innerState
     *            inner state
     * @param data
     *            buffer to use to read data
     * @param entityStream
     *            stream for reading the HTTP response entity
     * @throws StopRequest
     *             StopRequest
     */
    private void transferDataForWap(State state, InnerState innerState, byte[] data, InputStream entityStream)
            throws StopRequest {
        for (;;) {
            int bytesRead = readFromResponse(state, innerState, data, entityStream);
            if (bytesRead == -1) { // success, end of stream already reached
                handleEndOfStreamForWap(state, innerState);
                return;
            }
            state.mGotData = true;
            writeDataToDestination(state, data, bytesRead);
            innerState.mBytesSoFar += bytesRead;
            reportProgress(state, innerState);

            if (Constants.LOGVV) {
                Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar + " for " + mInfo.mUri);
            }
            checkPausedOrCanceled(state);
        }
    }

    /**
     * Called when we've reached the end of the HTTP response stream, to update
     * the database and check for consistency. For Wap.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     * @throws StopRequest
     *             StopRequest
     */
    private void handleEndOfStreamForWap(State state, InnerState innerState) throws StopRequest {
        // add by liuqingbiao ?
        // ????mimetype?
        // ??
        if (state.mMimeType.equalsIgnoreCase(Constants.MIMETYPE_APK) && state.mFilename != null
                && innerState.mBytesSoFar == mInfo.mTotalBytes) {
            if (!AppUtils.isAPK(state.mFilename)) {
                File f = new File(state.mFilename);
                f.delete();
                throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR, "" + state);
            }
            if (Constants.LOGV) {
                Log.d(Constants.TAG, "wapdownload  downloaded file is apk");
            }
        } // end
        ContentValues values = new ContentValues();
        values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
        if (innerState.mHeaderContentLength == null) {
            values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
        }
        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);

    }

    /**
     * Read HTTP response headers and take appropriate action, including setting
     * up the destination file and updating the database. For wap.
     * 
     * @param state
     *            state
     * @param innerState
     *            InnerState
     * @param response
     *            response
     * @throws StopRequest
     *             StopRequest
     */
    private void processResponseHeadersForWap(State state, InnerState innerState, HttpResponse response)
            throws StopRequest {
        synchronized (mInfo) {
            readResponseHeaders(state, innerState, response);

            try {
                state.mStream = new FileOutputStream(state.mFilename, true);
            } catch (FileNotFoundException exc) {
                throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
                        "while opening destination file: " + exc.toString(), exc);
            }
            if (Constants.LOGV) {
                Log.v(Constants.TAG, "wapdownload writing " + mInfo.mUri + " to " + state.mFilename);
            }
            updateDatabaseFromHeaders(state, innerState);
        }

        // check connectivity again now that we know the total size
        checkConnectivity(state);
    }

    /**
     * Check if current connectivity is valid for this request.
     * 
     * @param state
     *            state
     * @throws StopRequest
     *             StopRequest
     */
    private void checkConnectivity(State state) throws StopRequest {
        int networkUsable = mInfo.checkCanUseNetwork();
        if (networkUsable != DownloadInfo.NETWORK_OK) {
            int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
            if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
                status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
                mInfo.notifyPauseDueToSize(true);
            } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
                status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
                mInfo.notifyPauseDueToSize(false);
            }
            throw new StopRequest(status, mInfo.getLogMessageForNetworkError(networkUsable));
        }
    }

    /**
     * Transfer as much data as possible from the HTTP response to the
     * destination file.
     * 
     * @param state
     *            state.
     * @param innerState
     *            inner state
     * @param data
     *            buffer to use to read data
     * @param entityStream
     *            stream for reading the HTTP response entity
     * @throws StopRequest
     *             StopRequest
     * @throws RetryDownload
     *             RetryDownload
     */
    private void transferData(State state, InnerState innerState, byte[] data, InputStream entityStream)
            throws StopRequest, RetryDownload {
        for (;;) {
            int bytesRead = readFromResponse(state, innerState, data, entityStream);
            if (bytesRead == -1) { // success, end of stream already reached
                handleEndOfStream(state, innerState);
                return;
            }

            state.mGotData = true;
            writeDataToDestination(state, data, bytesRead);
            innerState.mBytesSoFar += bytesRead;
            reportProgress(state, innerState);

            if (Constants.LOGVV) {
                Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar + " for " + mInfo.mUri);
            }

            checkPausedOrCanceled(state);
        }
    }

    /**
     * Called after a successful completion to take any necessary action on the
     * downloaded file.
     * 
     * @param state
     *            state
     * @throws StopRequest
     *             StopRequest
     */
    private void finalizeDestinationFile(State state) throws StopRequest {
        if (isDrmFile(state)) {
            // transferToDrm(state); // delete by caohaitao
            Log.e("DownloadThread", "finalizeDestinationFile drm file failed.");
        } else {
            // make sure the file is readable
            // FileUtils.setPermissions(state.mFilename, 0644, -1, -1); //
            // delete by caohaitao
            syncDestination(state);
        }
    }

    /**
     * Called just before the thread finishes, regardless of status, to take any
     * necessary action on the downloaded file.
     * 
     * @param state
     *            state
     * @param finalStatus
     *            final status.
     */
    private void cleanupDestination(State state, int finalStatus) {
        closeDestination(state);
        if (state.mFilename != null && finalStatus == Downloads.Impl.STATUS_CANCELED) {
            File file = new File(state.mFilename);
            boolean deleted = file.delete();
            if (!deleted) {
                Log.w(Constants.TAG, "cleanupDestination delete file failed");
            }
            state.mFilename = null;
        }
    }

    /**
     * Sync the destination file to storage.
     * 
     * @param state
     *            state
     */
    private void syncDestination(State state) {
        FileOutputStream downloadedFileStream = null;
        try {
            downloadedFileStream = new FileOutputStream(state.mFilename, true);
            downloadedFileStream.getFD().sync();
        } catch (FileNotFoundException ex) {
            Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
        } catch (SyncFailedException ex) {
            Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
        } catch (IOException ex) {
            Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
        } catch (RuntimeException ex) {
            Log.w(Constants.TAG, "exception while syncing file: ", ex);
        } finally {
            if (downloadedFileStream != null) {
                try {
                    downloadedFileStream.close();
                } catch (IOException ex) {
                    Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
                } catch (RuntimeException ex) {
                    Log.w(Constants.TAG, "exception while closing file: ", ex);
                }
            }
        }
    }

    /**
     * true if the current download is a DRM file
     * 
     * @param state
     *            state
     * @return true if the current download is a DRM file
     */
    private boolean isDrmFile(State state) {
        return Constants.MIMETYPE_DRM_MESSAGE.equalsIgnoreCase(state.mMimeType);
    }

    /**
     * Transfer the downloaded destination file to the DRM store.
     * 
     * @param state
     *            state
     * @throws StopRequest
     *             StopRequest
     */
    private void transferToDrm(State state) throws StopRequest {
        // delete by caohaitao
        /*
         * File file = new File(state.mFilename); Intent item =
         * DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
         * file.delete();
         * 
         * if (item == null) { throw new
         * StopRequest(Downloads.Impl.STATUS_UNKNOWN_ERROR,
         * "unable to add file to DrmProvider"); } else { state.mFilename =
         * item.getDataString(); state.mMimeType = item.getType(); }
         */
    }

    /**
     * Close the destination output stream.
     * 
     * @param state
     *            state
     */
    private void closeDestination(State state) {
        try {
            // close the file
            if (state.mStream != null) {
                state.mStream.close();
                state.mStream = null;
            }
        } catch (IOException ex) {
            if (Constants.LOGV) {
                Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
            }
            // nothing can really be done if the file can't be closed
        }
    }

    /**
     * Check if the download has been paused or canceled, stopping the request
     * appropriately if it has been.
     * 
     * @param state
     *            state
     * @throws StopRequest
     *             StopRequest
     */
    private void checkPausedOrCanceled(State state) throws StopRequest {
        synchronized (mInfo) {
            if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
                throw new StopRequest(Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
            }
        }
        if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
            throw new StopRequest(Downloads.Impl.STATUS_CANCELED, "download canceled");
        }
    }

    /**
     * Report download progress through the database if necessary.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     */
    private void reportProgress(State state, InnerState innerState) {
        long now = mSystemFacade.currentTimeMillis();
        if (innerState.mBytesSoFar - innerState.mBytesNotified > Constants.MIN_PROGRESS_STEP
                && now - innerState.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
            ContentValues values = new ContentValues();
            values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
            innerState.mBytesNotified = innerState.mBytesSoFar;
            innerState.mTimeLastNotification = now;
        }
    }

    /**
     * Write a data buffer to the destination file.
     * 
     * @param state
     *            state
     * @param data
     *            buffer containing the data to write
     * @param bytesRead
     *            how many bytes to write from the buffer
     * @throws StopRequest
     *             StopRequest
     */
    private void writeDataToDestination(State state, byte[] data, int bytesRead) throws StopRequest {
        for (;;) {
            try {
                if (state.mStream == null) {
                    state.mStream = new FileOutputStream(state.mFilename, true);
                }
                state.mStream.write(data, 0, bytesRead);
                if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL && !isDrmFile(state)) {
                    closeDestination(state);
                }
                return;
            } catch (IOException ex) {
                if (mInfo.isOnCache()) {
                    if (Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
                        continue;
                    }
                } else if (!Helpers.isExternalMediaMounted()) {
                    throw new StopRequest(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
                            "external media not mounted while writing destination file");
                }

                long availableBytes = Helpers
                        .getAvailableBytes(Helpers.getFilesystemRoot(mContext, state.mFilename));
                if (availableBytes < bytesRead) {
                    throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
                            "insufficient space while writing destination file", ex);
                }
                throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
                        "while writing destination file: " + ex.toString(), ex);
            }
        }
    }

    /**
     * Called when we've reached the end of the HTTP response stream, to update
     * the database and check for consistency.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     * @throws StopRequest
     *             StopRequest
     * @throws RetryDownload
     *             RetryDownload
     */
    private void handleEndOfStream(State state, InnerState innerState) throws StopRequest, RetryDownload {
        // add by liuqingbiao ?
        // ????mimetype?
        // ??
        if (state.mMimeType.equalsIgnoreCase(Constants.MIMETYPE_APK) && state.mFilename != null) {
            // begain add by liuqingbiao
            // read-1??lighted??-1?
            File f = new File(state.mFilename);
            if (f.exists() && !TextUtils.isEmpty(innerState.mHeaderContentLength)) {
                int fs = (int) f.length();
                int len = Integer.valueOf(innerState.mHeaderContentLength).intValue();
                if (len > fs) {
                    if (Constants.LOGV) {
                        Log.d(Constants.TAG, "handleEndOfStream download ter, retry");
                    }
                    throw new RetryDownload();
                }
            }
            // end
            if (!AppUtils.isAPK(state.mFilename)) {
                File file = new File(state.mFilename);
                file.delete();
                throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR, "" + state);
            }
        } // end
        ContentValues values = new ContentValues();
        values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
        if (innerState.mHeaderContentLength == null) {
            values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
        }
        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);

        boolean lengthMismatched = (innerState.mHeaderContentLength != null)
                && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
        if (lengthMismatched) {
            if (cannotResume(innerState)) {
                throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME, "mismatched content length");
            } else {
                throw new StopRequest(getFinalStatusForHttpError(state), "closed socket before end of file");
            }
        }
    }

    /***
     * can not resume ?
     * 
     * @param innerState
     *            innerState
     * @return not return true
     */
    private boolean cannotResume(InnerState innerState) {
        return innerState.mBytesSoFar > 0 && !mInfo.mNoIntegrity && innerState.mHeaderETag == null;
    }

    /**
     * Read some data from the HTTP response stream, handling I/O errors.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     * @param data
     *            buffer to use to read data
     * @param entityStream
     *            stream for reading the HTTP response entity
     * @return the number of bytes actually read or -1 if the end of the stream
     *         has been reached
     * @throws StopRequest
     *             StopRequest
     */
    private int readFromResponse(State state, InnerState innerState, byte[] data, InputStream entityStream)
            throws StopRequest {
        try {
            return entityStream.read(data);
        } catch (IOException ex) {
            logNetworkState();
            ContentValues values = new ContentValues();
            values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
            if (cannotResume(innerState)) {
                String message = "while reading response: " + ex.toString()
                        + ", can't resume interrupted download with no ETag";
                throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME, message, ex);
            } else {
                throw new StopRequest(getFinalStatusForHttpError(state), "while reading response: " + ex.toString(),
                        ex);
            }
        }
    }

    /**
     * Open a stream for the HTTP response entity, handling I/O errors.
     * 
     * @param state
     *            state
     * @param response
     *            response
     * @throws StopRequest
     *             StopRequest
     * @return an InputStream to read the response entity
     */
    private InputStream openResponseEntity(State state, HttpResponse response) throws StopRequest {
        try {
            return response.getEntity().getContent();
        } catch (IOException ex) {
            logNetworkState();
            throw new StopRequest(getFinalStatusForHttpError(state), "while getting entity: " + ex.toString(), ex);
        }
    }

    /**
     * logNetworkState.
     */
    private void logNetworkState() {
        if (Constants.LOGX) {
            Log.i(Constants.TAG, "Net " + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up" : "Down"));
        }
    }

    /**
     * Read HTTP response headers and take appropriate action, including setting
     * up the destination file and updating the database.
     * 
     * @param state
     *            state
     * @param innerState
     *            InnerState
     * @param response
     *            response
     * @throws StopRequest
     *             StopRequest
     */
    private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
            throws StopRequest {
        if (innerState.mContinuingDownload) {
            // ignore response headers on resume requests
            return;
        }

        synchronized (mInfo) { // add by caoahitao fix SEARHBOX-65
            // ???Android????
            readResponseHeaders(state, innerState, response);

            try {
                state.mFilename = Helpers.generateSaveFile(mContext, mInfo.mUri, mInfo.mHint,
                        innerState.mHeaderContentDisposition, innerState.mHeaderContentLocation, state.mMimeType,
                        mInfo.mDestination,
                        (innerState.mHeaderContentLength != null) ? Long.parseLong(innerState.mHeaderContentLength)
                                : 0,
                        mInfo.mIsPublicApi);
            } catch (Helpers.GenerateSaveFileError exc) {
                throw new StopRequest(exc.mStatus, exc.mMessage);
            }
            try {
                state.mStream = new FileOutputStream(state.mFilename, true);
            } catch (FileNotFoundException exc) {
                throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
                        "while opening destination file: " + exc.toString(), exc);
            }
            if (Constants.LOGV) {
                Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
            }

            updateDatabaseFromHeaders(state, innerState);
        }

        // check connectivity again now that we know the total size
        checkConnectivity(state);
    }

    /**
     * Update necessary database fields based on values of HTTP response headers
     * that have been read.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     */
    private void updateDatabaseFromHeaders(State state, InnerState innerState) {
        ContentValues values = new ContentValues();
        values.put(Downloads.Impl.DATA, state.mFilename);
        if (innerState.mHeaderETag != null) {
            values.put(Constants.ETAG, innerState.mHeaderETag);
        }
        if (state.mMimeType != null) {
            values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
        }
        values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    }

    /**
     * Read headers from the HTTP response and store them into local state.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     * @param response
     *            response
     * @throws StopRequest
     *             StopRequest
     */
    private void readResponseHeaders(State state, InnerState innerState, HttpResponse response) throws StopRequest {
        Header header = response.getFirstHeader("Content-Disposition");
        if (header != null) {
            innerState.mHeaderContentDisposition = header.getValue();
        }
        header = response.getFirstHeader("Content-Location");
        if (header != null) {
            innerState.mHeaderContentLocation = header.getValue();
        }

        if (state.mMimeType == null) {
            header = response.getFirstHeader("Content-Type");
            if (header != null) {
                state.mMimeType = sanitizeMimeType(header.getValue());
            }
        }
        header = response.getFirstHeader("ETag");
        if (header != null) {
            innerState.mHeaderETag = header.getValue();
        }
        String headerTransferEncoding = null;
        header = response.getFirstHeader("Transfer-Encoding");
        if (header != null) {
            headerTransferEncoding = header.getValue();
        }
        if (headerTransferEncoding == null) {
            header = response.getFirstHeader("Content-Length");
            Header rangeHeader = response.getFirstHeader("Content-Range");
            if (rangeHeader != null) {
                String value = parseContentRange(rangeHeader);
                mInfo.mTotalBytes = Long.valueOf(value);
                innerState.mHeaderContentLength = value;
            } else if (header != null) {
                innerState.mHeaderContentLength = header.getValue();
                mInfo.mTotalBytes = Long.parseLong(innerState.mHeaderContentLength);
            }
        } else {
            // Ignore content-length with transfer-encoding - 2616 4.4 3
            if (Constants.LOGVV) {
                Log.v(Constants.TAG, "ignoring content-length because of xfer-encoding");
            }
        }
        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "Content-Disposition: " + innerState.mHeaderContentDisposition);
            Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
            Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
            Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
            Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
            Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
        }

        boolean noSizeInfo = innerState.mHeaderContentLength == null
                && (headerTransferEncoding == null || !headerTransferEncoding.equalsIgnoreCase("chunked"));
        if (!mInfo.mNoIntegrity && noSizeInfo) {
            throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR, "can't know size of download, giving up");
        }
    }

    /**
     * Check the HTTP response status and handle anything unusual (e.g. not
     * 200/206).
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     * @param response
     *            response
     * @throws StopRequest
     *             StopRequest
     * @throws RetryDownload
     *             RetryDownload
     */
    private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
            throws StopRequest, RetryDownload {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { // SUPPRESS
            // CHECKSTYLE
            handleServiceUnavailable(state, response);
        }
        if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) { // SUPPRESS CHECKSTYLE
            handleRedirect(state, response, statusCode);
        }

        int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS; // SUPPRESS CHECKSTYLE
        if (statusCode != expectedStatus) {
            handleOtherStatus(state, innerState, statusCode);
        }
    }

    /**
     * Handle a status that we don't know how to deal with properly.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     * @param statusCode
     *            statusCode
     * @throws StopRequest
     *             StopRequest
     */
    private void handleOtherStatus(State state, InnerState innerState, int statusCode) throws StopRequest {
        int finalStatus;
        if (Downloads.Impl.isStatusError(statusCode)) {
            finalStatus = statusCode;
        } else if (statusCode >= 300 && statusCode < 400) { // SUPPRESS
            // CHECKSTYLE
            finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
        } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
            finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
        } else {
            finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
        }
        throw new StopRequest(finalStatus, "http error " + statusCode);
    }

    /**
     * Handle a 3xx redirect status.
     * 
     * @param state
     *            state
     * @param response
     *            response
     * @param statusCode
     *            statusCode
     * @throws StopRequest
     *             StopRequest
     * @throws RetryDownload
     *             RetryDownload
     */
    private void handleRedirect(State state, HttpResponse response, int statusCode)
            throws StopRequest, RetryDownload {
        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
        }
        if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
            throw new StopRequest(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS, "too many redirects");
        }
        Header header = response.getFirstHeader("Location");
        if (header == null) {
            return;
        }
        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "Location :" + header.getValue());
        }

        String newUri;
        try {
            newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
        } catch (URISyntaxException ex) {
            if (Constants.LOGV) {
                Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue() + " for " + mInfo.mUri);
            }
            throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR, "Couldn't resolve redirect URI");
        }
        ++state.mRedirectCount;
        state.mRequestUri = newUri;
        if (statusCode == 301 || statusCode == 303) { // SUPPRESS CHECKSTYLE
            // use the new URI for all future requests (should a retry/resume be
            // necessary)
            state.mNewUri = newUri;
        }
        throw new RetryDownload();
    }

    /**
     * Handle a 503 Service Unavailable status by processing the Retry-After
     * header.
     * 
     * @param state
     *            state
     * @param response
     *            response
     * @throws StopRequest
     *             StopRequest
     */
    private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "got HTTP response code 503");
        }
        state.mCountRetry = true;
        Header header = response.getFirstHeader("Retry-After");
        if (header != null) {
            try {
                if (Constants.LOGVV) {
                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
                }
                state.mRetryAfter = Integer.parseInt(header.getValue());
                if (state.mRetryAfter < 0) {
                    state.mRetryAfter = 0;
                } else {
                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
                    }
                    state.mRetryAfter += Helpers.RANDOM.nextInt(Constants.MIN_RETRY_AFTER + 1);
                    state.mRetryAfter *= 1000; // SUPPRESS CHECKSTYLE
                }
            } catch (NumberFormatException ex) {
                // ignored - retryAfter stays 0 in this case.
                ex.printStackTrace();
            }
        }
        throw new StopRequest(Downloads.Impl.STATUS_WAITING_TO_RETRY,
                "got 503 Service Unavailable, will retry later");
    }

    /**
     * Send the request to the server, handling any I/O exceptions.
     * 
     * @param state
     *            state
     * @param client
     *            client
     * @param request
     *            request
     * @return sendRequest
     * @throws StopRequest
     *             StopRequest
     */
    private HttpResponse sendRequest(State state, HttpClient client, HttpGet request) throws StopRequest {
        try {
            return client.execute(request);
        } catch (IllegalArgumentException ex) {
            throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
                    "while trying to execute request: " + ex.toString(), ex);
        } catch (IOException ex) {
            logNetworkState();
            throw new StopRequest(getFinalStatusForHttpError(state),
                    "while trying to execute request: " + ex.toString(), ex);
        }
    }

    /**
     * getFinalStatusForHttpError
     * 
     * @param state
     *            state
     * @return getFinalStatusForHttpError
     */
    private int getFinalStatusForHttpError(State state) {
        if (!Helpers.isNetworkAvailable(mSystemFacade)) {
            return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
        } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
            state.mCountRetry = true;
            return Downloads.Impl.STATUS_WAITING_TO_RETRY;
        } else {
            Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
            return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
        }
    }

    /**
     * Prepare the destination file to receive data. If the file already exists,
     * we'll set up appropriately for resumption.
     * 
     * @param state
     *            state
     * @param innerState
     *            innerState
     * @throws StopRequest
     *             StopRequest
     */
    private void setupDestinationFile(State state, InnerState innerState) throws StopRequest {
        if (state.mFilename != null) { // only true if we've already run a
            // thread for this download
            if (!Helpers.isFilenameValid(state.mFilename)) {
                // this should never happen
                throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
                        "found invalid internal destination filename:" + state.mFilename);
            }
            // We're resuming a download that got interrupted
            File f = new File(state.mFilename);
            if (f.exists()) {
                long fileLength = f.length();
                if (fileLength == 0) {
                    // The download hadn't actually started, we can restart from
                    // scratch
                    boolean deleted = f.delete();
                    if (!deleted) {
                        Log.v(Constants.TAG, "setupDestinationFile delete file failed");
                    }
                    state.mFilename = null;
                } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
                    // This should've been caught upon failure
                    boolean deleted = f.delete();
                    if (!deleted) {
                        Log.v(Constants.TAG, "setupDestinationFile delete file failed");
                    }
                    throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
                            "Trying to resume a download that can't be resumed");
                } else {
                    // All right, we'll be able to resume this download
                    try {
                        state.mStream = new FileOutputStream(state.mFilename, true);
                    } catch (FileNotFoundException exc) {
                        throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
                                "while opening destination for resuming: " + exc.toString(), exc);
                    }
                    innerState.mBytesSoFar = (int) fileLength;
                    if (mInfo.mTotalBytes != -1) {
                        innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
                    }
                    innerState.mHeaderETag = mInfo.mETag;
                    innerState.mContinuingDownload = true;
                }
            }
        }

        if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
                && !isDrmFile(state)) {
            closeDestination(state);
        }
    }

    /**
     * Add custom headers for this download to the HTTP request.
     * 
     * @param innerState
     *            innerState
     * @param request
     *            request
     */
    private void addRequestHeaders(InnerState innerState, HttpGet request) {
        for (Pair<String, String> header : mInfo.getHeaders()) {
            request.addHeader(header.mFirst, header.mSecond);
        }

        if (innerState.mContinuingDownload) {
            if (innerState.mHeaderETag != null) {
                request.addHeader("If-Match", innerState.mHeaderETag);
            }
            request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
        }
    }

    /**
     * Stores information about the completed download, and notifies the
     * initiating application.
     * 
     * @param status
     *            status
     * @param countRetry
     *            countRetry
     * @param retryAfter
     *            retryAfter
     * @param redirectCount
     *            redirectCount
     * @param gotData
     *            gotData
     * @param filename
     *            filename
     * @param uri
     *            uri
     * @param mimeType
     *            mimeType
     */
    private void notifyDownloadCompleted(int status, boolean countRetry, int retryAfter, int redirectCount,
            boolean gotData, String filename, String uri, String mimeType) {
        notifyThroughDatabase(status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
        if (Downloads.Impl.isStatusCompleted(status)) {
            mInfo.sendIntentIfRequested();
        }
    }

    /**
     * notify Through Database
     * 
     * @param status
     *            status
     * @param countRetry
     *            countRetry
     * @param retryAfter
     *            retryAfter
     * @param redirectCount
     *            redirectCount
     * @param gotData
     *            gotData
     * @param filename
     *            filename
     * @param uri
     *            uri
     * @param mimeType
     *            mimeType
     */
    private void notifyThroughDatabase(int status, boolean countRetry, int retryAfter, int redirectCount,
            boolean gotData, String filename, String uri, String mimeType) {
        ContentValues values = new ContentValues();
        values.put(Downloads.Impl.COLUMN_STATUS, status);
        // begin ?????????? added by liuqingbiao
        // values.put(Downloads.Impl.DATA, filename);
        // end
        if (uri != null) {
            values.put(Downloads.Impl.COLUMN_URI, uri);
        }
        values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
        values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
        values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28)); // SUPPRESS CHECKSTYLE
        if (!countRetry) {
            values.put(Constants.FAILED_CONNECTIONS, 0);
        } else if (gotData) {
            values.put(Constants.FAILED_CONNECTIONS, 1);
        } else {
            values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
        }
        // ?? by liuqingbiao
        values.put(Constants.FAILED_REASON, mInfo.mFailedReason);

        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
    }

    /**
     * Clean up a mimeType string so it can be used to dispatch an intent to
     * view a downloaded asset.
     * 
     * @param mimeType
     *            either null or one or more mime types (semi colon separated).
     * @return null if mimeType was null. Otherwise a string which represents a
     *         single mimetype in lowercase and with surrounding whitespaces
     *         trimmed.
     */
    private static String sanitizeMimeType(String mimeType) {
        try {
            mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);

            final int semicolonIndex = mimeType.indexOf(';');
            if (semicolonIndex != -1) {
                mimeType = mimeType.substring(0, semicolonIndex);
            }
            return mimeType;
        } catch (NullPointerException npe) {
            return null;
        }
    }
}