/* * 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.android.providers.transfers; import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST; import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME; import static android.provider.Downloads.Impl.STATUS_FILE_ERROR; import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR; import static android.provider.Downloads.Impl.STATUS_SUCCESS; import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS; import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK; import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY; import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.providers.transfers.Constants.TAG; import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; import static java.net.HttpURLConnection.HTTP_MOVED_PERM; import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_PARTIAL; import static java.net.HttpURLConnection.HTTP_SEE_OTHER; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.drm.DrmManagerClient; import android.drm.DrmOutputStream; import android.net.ConnectivityManager; import android.net.INetworkPolicyListener; import android.net.NetworkInfo; import android.net.NetworkPolicyManager; import android.net.TrafficStats; import android.os.FileUtils; import android.os.PowerManager; import android.os.Process; import android.os.SystemClock; import android.os.WorkSource; import android.provider.Downloads; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.android.providers.transfers.Constants; import com.android.providers.transfers.TransferInfo; import com.android.providers.transfers.TransferProvider; import com.android.providers.transfers.StopRequestException; import com.android.providers.transfers.StorageManager; import com.android.providers.transfers.SystemFacade; import com.android.providers.transfers.TransferInfo.NetworkState; import libcore.io.IoUtils; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; /** * Task which executes a given {@link DownloadInfo}: making network requests, * persisting data to disk, and updating {@link DownloadProvider}. */ public abstract class TransferThread implements Runnable { // TODO: bind each transfer to a specific network interface to avoid state // checking races once we have ConnectivityManager API protected static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; protected static final int HTTP_TEMP_REDIRECT = 307; protected static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS); protected final Context mContext; protected final TransferInfo mInfo; protected final SystemFacade mSystemFacade; protected final StorageManager mStorageManager; protected final TransferNotifier mNotifier; protected volatile boolean mPolicyDirty; public TransferThread(Context context, SystemFacade systemFacade, TransferInfo info, StorageManager storageManager, TransferNotifier notifier) { mContext = context; mSystemFacade = systemFacade; mInfo = info; mStorageManager = storageManager; mNotifier = notifier; } /** * Returns the user agent provided by the initiating app, or use the default one */ private String userAgent() { String userAgent = mInfo.mUserAgent; if (userAgent == null) { userAgent = Constants.DEFAULT_USER_AGENT; } return userAgent; } /** * State for the entire run() method. */ static class State { public String mFilename; public String mMimeType; public int mRetryAfter = 0; public boolean mGotData = false; public String mRequestUri; public long mTotalBytes = -1; public long mCurrentBytes = 0; public String mHeaderETag; public boolean mContinuingTransfer = false; public long mBytesNotified = 0; public long mTimeLastNotification = 0; public int mNetworkType = ConnectivityManager.TYPE_NONE; /** Historical bytes/second speed of this transfer. */ public long mSpeed; /** Time when current sample started. */ public long mSpeedSampleStart; /** Bytes transferred since current sample started. */ public long mSpeedSampleBytes; public long mContentLength = -1; public String mContentDisposition; public String mContentLocation; public int mRedirectionCount; public URL mUrl; public State(TransferInfo info) { mMimeType = Intent.normalizeMimeType(info.mMimeType); mRequestUri = info.mUri; mFilename = info.mFileName; mTotalBytes = info.mTotalBytes; mCurrentBytes = info.mCurrentBytes; } public void resetBeforeExecute() { // Reset any state from previous execution mContentLength = -1; mContentDisposition = null; mContentLocation = null; mRedirectionCount = 0; } } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); try { runInternal(); } finally { mNotifier.notifyTransferSpeed(mInfo.mId, 0); } } abstract void runInternal(); /** * Fully execute a single transfer request. Setup and send the request, * handle the response, and transfer the data to the destination file. */ abstract void executeTransfer(State state) throws StopRequestException; /** * Transfer data from the given connection to the destination file. */ abstract void transferData(State state, HttpURLConnection conn) throws StopRequestException; /** * Check if current connectivity is valid for this request. */ private void checkConnectivity() throws StopRequestException { // checking connectivity will apply current policy mPolicyDirty = false; final NetworkState networkUsable = mInfo.checkCanUseNetwork(); if (networkUsable != NetworkState.OK) { int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(true); } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(false); } throw new StopRequestException(status, networkUsable.name()); } } /** * Transfer as much data as possible from the HTTP response to the * destination file. */ abstract void transferData(State state, InputStream in, OutputStream out) throws StopRequestException; /** * Check if the transfer has been paused or canceled, stopping the request appropriately if it * has been. */ private void checkPausedOrCanceled(State state) throws StopRequestException { synchronized (mInfo) { if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) { throw new StopRequestException( Downloads.Impl.STATUS_PAUSED_BY_APP, "transfer paused by owner"); } if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) { throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "transfer canceled"); } } // if policy has been changed, trigger connectivity check if (mPolicyDirty) { checkConnectivity(); } } /** * Report transfer progress through the database if necessary. */ private void reportProgress(State state) { final long now = SystemClock.elapsedRealtime(); final long sampleDelta = now - state.mSpeedSampleStart; if (sampleDelta > 500) { final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000) / sampleDelta; if (state.mSpeed == 0) { state.mSpeed = sampleSpeed; } else { state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4; } // Only notify once we have a full sample window if (state.mSpeedSampleStart != 0) { mNotifier.notifyTransferSpeed(mInfo.mId, state.mSpeed); } state.mSpeedSampleStart = now; state.mSpeedSampleBytes = state.mCurrentBytes; } if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP && now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); state.mBytesNotified = state.mCurrentBytes; state.mTimeLastNotification = now; } } /** * Called when we've reached the end of the HTTP response stream, to update the database and * check for consistency. */ private void handleEndOfStream(State state) throws StopRequestException { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); if (state.mContentLength == -1) { values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes); } mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); final boolean lengthMismatched = (state.mContentLength != -1) && (state.mCurrentBytes != state.mContentLength); if (lengthMismatched) { if (cannotResume(state)) { throw new StopRequestException(STATUS_CANNOT_RESUME, "mismatched content length; unable to resume"); } else { throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "closed socket before end of file"); } } } private boolean cannotResume(State state) { return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null) || TransferDrmHelper.isDrmConvertNeeded(state.mMimeType); } /** * Read some data from the HTTP response stream, handling I/O errors. * @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 */ private int readFromResponse(State state, byte[] data, InputStream entityStream) throws StopRequestException { try { return entityStream.read(data); } catch (IOException ex) { // TODO: handle stream errors the same as other retries if ("unexpected end of stream".equals(ex.getMessage())) { return -1; } ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); if (cannotResume(state)) { throw new StopRequestException(STATUS_CANNOT_RESUME, "Failed reading response: " + ex + "; unable to resume", ex); } else { throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Failed reading response: " + ex, ex); } } } /** * Prepare target file based on given network response. Derives filename and * target size as needed. */ private void processResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { // TODO: fallocate the entire file if header gave us specific length readResponseHeaders(state, conn); state.mFilename = Helpers.generateSaveFile( mContext, mInfo.mUri, mInfo.mHint, state.mContentDisposition, state.mContentLocation, state.mMimeType, mInfo.mDestination, state.mContentLength, mStorageManager); updateDatabaseFromHeaders(state); // check connectivity again now that we know the total size checkConnectivity(); } /** * Update necessary database fields based on values of HTTP response headers that have been * read. */ private void updateDatabaseFromHeaders(State state) { ContentValues values = new ContentValues(); values.put(Downloads.Impl._DATA, state.mFilename); if (state.mHeaderETag != null) { values.put(Constants.ETAG, state.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. */ private void readResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { state.mContentDisposition = conn.getHeaderField("Content-Disposition"); state.mContentLocation = conn.getHeaderField("Content-Location"); if (state.mMimeType == null) { state.mMimeType = Intent.normalizeMimeType(conn.getContentType()); } state.mHeaderETag = conn.getHeaderField("ETag"); final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); if (transferEncoding == null) { state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1); } else { Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined"); state.mContentLength = -1; } state.mTotalBytes = state.mContentLength; mInfo.mTotalBytes = state.mContentLength; final boolean noSizeInfo = state.mContentLength == -1 && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked")); if (!mInfo.mNoIntegrity && noSizeInfo) { throw new StopRequestException(STATUS_CANNOT_RESUME, "can't know size of transfer, giving up"); } } private void parseRetryAfterHeaders(State state, HttpURLConnection conn) { state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1); 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.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); state.mRetryAfter *= 1000; } } /** * Add custom headers for this transfer to the HTTP request. */ private void addRequestHeaders(State state, HttpURLConnection conn) { for (Pair<String, String> header : mInfo.getHeaders()) { conn.addRequestProperty(header.first, header.second); } // Only splice in user agent when not already defined if (conn.getRequestProperty("User-Agent") == null) { conn.addRequestProperty("User-Agent", userAgent()); } // Defeat transparent gzip compression, since it doesn't allow us to // easily resume partial transfers. conn.setRequestProperty("Accept-Encoding", "identity"); if (state.mContinuingTransfer) { if (state.mHeaderETag != null) { conn.addRequestProperty("If-Match", state.mHeaderETag); } conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-"); } } /** * Stores information about the completed transfer, and notifies the initiating application. */ private void notifyTransferCompleted( State state, int finalStatus, String errorMsg, int numFailed) { notifyThroughDatabase(state, finalStatus, errorMsg, numFailed); if (Downloads.Impl.isStatusCompleted(finalStatus)) { mInfo.sendIntentIfRequested(); } } private void notifyThroughDatabase( State state, int finalStatus, String errorMsg, int numFailed) { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_STATUS, finalStatus); values.put(Downloads.Impl._DATA, state.mFilename); values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType); values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis()); values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed); values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter); if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) { values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri); } // save the error message. could be useful to developers. if (!TextUtils.isEmpty(errorMsg)) { values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg); } mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); } private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() { @Override public void onUidRulesChanged(int uid, int uidRules) { // caller is NPMS, since we only register with them if (uid == mInfo.mUid) { mPolicyDirty = true; } } @Override public void onMeteredIfacesChanged(String[] meteredIfaces) { // caller is NPMS, since we only register with them mPolicyDirty = true; } @Override public void onRestrictBackgroundChanged(boolean restrictBackground) { // caller is NPMS, since we only register with them mPolicyDirty = true; } }; public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { try { return Long.parseLong(conn.getHeaderField(field)); } catch (NumberFormatException e) { return defaultValue; } } /** * Return if given status is eligible to be treated as * {@link com.android.providers.transfers.Transfers.Impl#STATUS_WAITING_TO_RETRY}. */ public static boolean isStatusRetryable(int status) { switch (status) { case STATUS_HTTP_DATA_ERROR: case HTTP_UNAVAILABLE: case HTTP_INTERNAL_ERROR: return true; default: return false; } } }