/** * Write a data buffer to the destination file. * @param data buffer containing the data to write * @param bytesRead how many bytes to write from the buffer */ private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out) throws StopRequestException { mStorageManager.verifySpaceBeforeWritingToFile( mInfo.mDestination, state.mFilename, bytesRead); boolean forceVerified = false; while (true) { try { out.write(data, 0, bytesRead); return; } catch (IOException ex) { // TODO: better differentiate between DRM and disk failures if (!forceVerified) { // couldn't write to file. are we out of space? check. mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead); forceVerified = true; } else { throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, "Failed to write data: " + ex); } } } }
File locateDestinationDirectory(String mimeType, int destination, long contentLength) throws StopRequestException { switch (destination) { case Downloads.Impl.DESTINATION_CACHE_PARTITION: case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: return mTransferDataDir; case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION: return mSystemCacheDir; case Downloads.Impl.DESTINATION_EXTERNAL: File base = new File(mExternalStorageDir.getPath() + Constants.DEFAULT_DL_SUBDIR); if (!base.isDirectory() && !base.mkdir()) { // Can't create transfer directory, e.g. because a file called "transfer" // already exists at the root level, or the SD card filesystem is read-only. throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, "unable to create external transfers directory " + base.getPath()); } return base; default: throw new IllegalStateException("unexpected value for destination: " + destination); } }
/** * Check that the file URI provided for DESTINATION_FILE_URI is valid. */ private void checkFileUriDestination(ContentValues values) { String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT); if (fileUri == null) { throw new IllegalArgumentException( "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT"); } Uri uri = Uri.parse(fileUri); String scheme = uri.getScheme(); if (scheme == null || !scheme.equals("file")) { throw new IllegalArgumentException("Not a file URI: " + uri); } final String path = uri.getPath(); if (path == null) { throw new IllegalArgumentException("Invalid file URI: " + uri); } try { final String canonicalPath = new File(path).getCanonicalPath(); final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath(); if (!canonicalPath.startsWith(externalPath)) { throw new SecurityException("Destination must be on external storage: " + uri); } } catch (IOException e) { throw new SecurityException("Problem resolving path: " + uri); } }
/** * Insert request headers for a transfer into the DB. */ private void insertRequestHeaders(SQLiteDatabase db, long transferId, ContentValues values) { ContentValues rowValues = new ContentValues(); rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, transferId); for (Map.Entry<String, Object> entry : values.valueSet()) { String key = entry.getKey(); if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) { String headerLine = entry.getValue().toString(); if (!headerLine.contains(":")) { throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine); } String[] parts = headerLine.split(":", 2); rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim()); rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim()); db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues); } } }
private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs, int uriMatch) { SqlSelection selection = new SqlSelection(); selection.appendClause(where, whereArgs); if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID || uriMatch == PUBLIC_DOWNLOAD_ID) { selection.appendClause(Downloads.Impl._ID + " = ?", getTransferIdFromUri(uri)); } if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID) && getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) { selection.appendClause( Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?", Binder.getCallingUid(), Binder.getCallingUid()); } return selection; }
private void readRequestHeaders(TransferInfo info) { info.mRequestHeaders.clear(); Uri headerUri = Uri.withAppendedPath( info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT); Cursor cursor = mResolver.query(headerUri, null, null, null, null); try { int headerIndex = cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER); int valueIndex = cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE); for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { addHeader(info, cursor.getString(headerIndex), cursor.getString(valueIndex)); } } finally { cursor.close(); } if (info.mCookies != null) { addHeader(info, "Cookie", info.mCookies); } if (info.mReferer != null) { addHeader(info, "Referer", info.mReferer); } }
public void sendIntentIfRequested() { if (mPackage == null) { return; } Intent intent; if (mIsPublicApi) { intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE); intent.setPackage(mPackage); intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId); } else { // legacy behavior if (mClass == null) { return; } intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED); intent.setClassName(mPackage, mClass); if (mExtras != null) { intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras); } // We only send the content: URI, for security reasons. Otherwise, malicious // applications would have an easier time spoofing transfer results by // sending spoofed intents. intent.setData(getMyDownloadsUri()); } mSystemFacade.sendBroadcast(intent); }
/** * Query and return status of requested transfer. */ public static int queryTransferStatus(ContentResolver resolver, long id) { final Cursor cursor = resolver.query( ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), new String[] { Downloads.Impl.COLUMN_STATUS }, null, null, null); try { if (cursor.moveToFirst()) { return cursor.getInt(0); } else { // TODO: increase strictness of value returned for unknown // transfers; this is safe default for now. return Downloads.Impl.STATUS_PENDING; } } finally { cursor.close(); } }
/** * 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()); } }
/** * 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(); } }
/** * 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"); } } }
/** * 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); } } }
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 void showDialog(Cursor cursor) { int size = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES)); String sizeString = Formatter.formatFileSize(this, size); String queueText = getString(R.string.button_queue_for_wifi); boolean isWifiRequired = mCurrentIntent.getExtras().getBoolean(TransferInfo.EXTRA_IS_WIFI_REQUIRED); AlertDialog.Builder builder = new AlertDialog.Builder(this, AlertDialog.THEME_HOLO_DARK); if (isWifiRequired) { builder.setTitle(R.string.wifi_required_title) .setMessage(getString(R.string.wifi_required_body, sizeString, queueText)) .setPositiveButton(R.string.button_queue_for_wifi, this) .setNegativeButton(R.string.button_cancel_download, this); } else { builder.setTitle(R.string.wifi_recommended_title) .setMessage(getString(R.string.wifi_recommended_body, sizeString, queueText)) .setPositiveButton(R.string.button_start_now, this) .setNegativeButton(R.string.button_queue_for_wifi, this); } mDialog = builder.setOnCancelListener(this).show(); }
/** * Called just before the thread finishes, regardless of status, to take any necessary action on * the transfered file. */ private void cleanupDestination(State state, int finalStatus) { if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) { if (Constants.LOGVV) { Log.d(TAG, "cleanupDestination() deleting " + state.mFilename); } new File(state.mFilename).delete(); state.mFilename = null; } }
void verifySpace(int destination, String path, long length) throws StopRequestException { resetBytesTransferedSinceLastCheckOnSpace(); File dir = null; if (Constants.LOGV) { Log.i(Constants.TAG, "in verifySpace, destination: " + destination + ", path: " + path + ", length: " + length); } if (path == null) { throw new IllegalArgumentException("path can't be null"); } switch (destination) { case Downloads.Impl.DESTINATION_CACHE_PARTITION: case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: dir = mTransferDataDir; break; case Downloads.Impl.DESTINATION_EXTERNAL: dir = mExternalStorageDir; break; case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION: dir = mSystemCacheDir; break; case Downloads.Impl.DESTINATION_FILE_URI: if (path.startsWith(mExternalStorageDir.getPath())) { dir = mExternalStorageDir; } else if (path.startsWith(mTransferDataDir.getPath())) { dir = mTransferDataDir; } else if (path.startsWith(mSystemCacheDir.getPath())) { dir = mSystemCacheDir; } break; } if (dir == null) { throw new IllegalStateException("invalid combination of destination: " + destination + ", path: " + path); } findSpace(dir, length, destination); }
/** * insert() now ensures these four columns are never null for new transfers, so this method * makes that true for existing columns, so that code can rely on this assumption. */ private void fillNullValues(SQLiteDatabase db) { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0); fillNullValuesForColumn(db, values); values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1); fillNullValuesForColumn(db, values); values.put(Downloads.Impl.COLUMN_TITLE, ""); fillNullValuesForColumn(db, values); values.put(Downloads.Impl.COLUMN_DESCRIPTION, ""); fillNullValuesForColumn(db, values); }
/** * Set all existing transfers to the cache partition to be invisible in the transfers UI. */ private void makeCacheDownloadsInvisible(SQLiteDatabase db) { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, false); String cacheSelection = Downloads.Impl.COLUMN_DESTINATION + " != " + Downloads.Impl.DESTINATION_EXTERNAL; db.update(DB_TABLE, values, cacheSelection, null); }
private void createHeadersTable(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE); db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," + Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," + Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" + ");"); }
/** * Returns the content-provider-style MIME types of the various * types accessible through this content provider. */ @Override public String getType(final Uri uri) { int match = sURIMatcher.match(uri); switch (match) { case MY_DOWNLOADS: case ALL_DOWNLOADS: { return DOWNLOAD_LIST_TYPE; } case MY_DOWNLOADS_ID: case ALL_DOWNLOADS_ID: case PUBLIC_DOWNLOAD_ID: { // return the mimetype of this id from the database final String id = getTransferIdFromUri(uri); final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); final String mimeType = DatabaseUtils.stringForQuery(db, "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE + " WHERE " + Downloads.Impl._ID + " = ?", new String[]{id}); if (TextUtils.isEmpty(mimeType)) { return DOWNLOAD_TYPE; } else { return mimeType; } } default: { if (Constants.LOGV) { Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri); } throw new IllegalArgumentException("Unknown URI: " + uri); } } }
/** * Handle a query for the custom request headers registered for a transfer. */ private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) { String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + getTransferIdFromUri(uri); String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER, Downloads.Impl.RequestHeaders.COLUMN_VALUE}; return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where, null, null, null, null); }
/** * Delete request headers for transfers matching the given query. */ private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) { String[] projection = new String[] {Downloads.Impl._ID}; Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null); try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { long id = cursor.getLong(0); String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id; db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null); } } finally { cursor.close(); } }
/** * Deletes a row in the database */ @Override public int delete(final Uri uri, final String where, final String[] whereArgs) { Helpers.validateSelection(where, sAppReadableColumnsSet); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; int match = sURIMatcher.match(uri); switch (match) { case MY_DOWNLOADS: case MY_DOWNLOADS_ID: case ALL_DOWNLOADS: case ALL_DOWNLOADS_ID: SqlSelection selection = getWhereClause(uri, where, whereArgs, match); deleteRequestHeaders(db, selection.getSelection(), selection.getParameters()); final Cursor cursor = db.query(DB_TABLE, new String[] { Downloads.Impl._ID }, selection.getSelection(), selection.getParameters(), null, null, null); try { while (cursor.moveToNext()) { final long id = cursor.getLong(0); TransferStorageProvider.onTransferProviderDelete(getContext(), id); } } finally { IoUtils.closeQuietly(cursor); } count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters()); break; default: Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri); throw new UnsupportedOperationException("Cannot delete URI: " + uri); } notifyContentChanged(uri, match); return count; }
@Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120); pw.println("Downloads updated in last hour:"); pw.increaseIndent(); final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS; final Cursor cursor = db.query(DB_TABLE, null, Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null, Downloads.Impl._ID + " ASC"); try { final String[] cols = cursor.getColumnNames(); final int idCol = cursor.getColumnIndex(BaseColumns._ID); while (cursor.moveToNext()) { pw.println("Transfer #" + cursor.getInt(idCol) + ":"); pw.increaseIndent(); for (int i = 0; i < cols.length; i++) { // Omit sensitive data when dumping if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) { continue; } pw.printPair(cols[i], cursor.getString(i)); } pw.println(); pw.decreaseIndent(); } } finally { cursor.close(); } pw.decreaseIndent(); }
private void logVerboseOpenFileInfo(Uri uri, String mode) { Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode + ", uid: " + Binder.getCallingUid()); Cursor cursor = query(Downloads.Impl.CONTENT_URI, new String[] { "_id" }, null, null, "_id"); if (cursor == null) { Log.v(Constants.TAG, "null cursor in openFile"); } else { if (!cursor.moveToFirst()) { Log.v(Constants.TAG, "empty cursor in openFile"); } else { do { Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available"); } while(cursor.moveToNext()); } cursor.close(); } cursor = query(uri, new String[] { "_data" }, null, null, null); if (cursor == null) { Log.v(Constants.TAG, "null cursor in openFile"); } else { if (!cursor.moveToFirst()) { Log.v(Constants.TAG, "empty cursor in openFile"); } else { String filename = cursor.getString(0); Log.v(Constants.TAG, "filename in openFile: " + filename); if (new java.io.File(filename).isFile()) { Log.v(Constants.TAG, "file exists in openFile"); } } cursor.close(); } }
/** * Returns whether this transfer should be enqueued. */ private boolean isReadyToTransfer() { if (mControl == Downloads.Impl.CONTROL_PAUSED) { // the transfer is paused, so it's not going to start return false; } switch (mStatus) { case 0: // status hasn't been initialized yet, this is a new transfer case Downloads.Impl.STATUS_PENDING: // transfer is explicit marked as ready to start case Downloads.Impl.STATUS_RUNNING: // transfer interrupted (process killed etc) while // running, without a chance to update the database return true; case Downloads.Impl.STATUS_WAITING_FOR_NETWORK: case Downloads.Impl.STATUS_QUEUED_FOR_WIFI: return checkCanUseNetwork() == NetworkState.OK; case Downloads.Impl.STATUS_WAITING_TO_RETRY: // transfer was waiting for a delayed restart final long now = mSystemFacade.currentTimeMillis(); return restartTime(now) <= now; case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR: // is the media mounted? return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR: // avoids repetition of retrying transfer return false; } return false; }
/** * Returns whether this transfer has a visible notification after * completion. */ public boolean hasCompletionNotification() { if (!Downloads.Impl.isStatusCompleted(mStatus)) { return false; } if (mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) { return true; } return false; }
private boolean isRoamingAllowed() { if (mIsPublicApi) { return mAllowRoaming; } else { // legacy behavior return mDestination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING; } }
/** * Return time when this transfer will be ready for its next action, in * milliseconds after given time. * * @return If {@code 0}, transfer is ready to proceed immediately. If * {@link Long#MAX_VALUE}, then transfer has no future actions. */ public long nextActionMillis(long now) { if (Downloads.Impl.isStatusCompleted(mStatus)) { return Long.MAX_VALUE; } if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) { return 0; } long when = restartTime(now); if (when <= now) { return 0; } return when - now; }
/** * Returns whether a file should be scanned */ public boolean shouldScanFile() { return (mMediaScanned == 0) && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL || mDestination == Downloads.Impl.DESTINATION_FILE_URI || mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) && Downloads.Impl.isStatusSuccess(mStatus); }
/** * Mark the given {@link TransferManager#COLUMN_ID} as being acknowledged by * user so it's not renewed later. */ private void hideNotification(Context context, long id) { final int status; final int visibility; final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); try { if (cursor.moveToFirst()) { status = getInt(cursor, Downloads.Impl.COLUMN_STATUS); visibility = getInt(cursor, Downloads.Impl.COLUMN_VISIBILITY); } else { Log.w(TAG, "Missing details for transfer " + id); return; } } finally { cursor.close(); } if (Downloads.Impl.isStatusCompleted(status) && (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)) { final ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_VISIBILITY, Downloads.Impl.VISIBILITY_VISIBLE); context.getContentResolver().update(uri, values, null, null); } }
/** * Creates a filename (where the file should be saved) from info about a transfer. */ static String generateSaveFile( Context context, String url, String hint, String contentDisposition, String contentLocation, String mimeType, int destination, long contentLength, StorageManager storageManager) throws StopRequestException { if (contentLength < 0) { contentLength = 0; } String path; File base = null; if (destination == Downloads.Impl.DESTINATION_FILE_URI) { path = Uri.parse(hint).getPath(); } else { base = storageManager.locateDestinationDirectory(mimeType, destination, contentLength); path = chooseFilename(url, hint, contentDisposition, contentLocation, destination); } storageManager.verifySpace(destination, path, contentLength); if (TransferDrmHelper.isDrmConvertNeeded(mimeType)) { path = TransferDrmHelper.modifyDrmFwLockFileExtension(path); } path = getFullPath(path, mimeType, destination, base); return path; }
@Override public void onScanCompleted(String path, Uri uri) { final ScanRequest req; synchronized (mConnection) { req = mPending.remove(path); } if (req == null) { Log.w(TAG, "Missing request for path " + path); return; } // Update scanned column, which will kick off a database update pass, // eventually deciding if overall service is ready for teardown. final ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1); if (uri != null) { values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString()); } final ContentResolver resolver = mContext.getContentResolver(); final Uri transferUri = ContentUris.withAppendedId( Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id); final int rows = resolver.update(transferUri, values, null, null); if (rows == 0) { // Local row disappeared during scan; transfer was probably deleted // so clean up now-orphaned media entry. resolver.delete(uri, null, null); } }
/** * 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; } }
/** * 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); }
/** * 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(); } }
/** * Initializes the service when it is first created */ @Override public void onCreate() { super.onCreate(); if (Constants.LOGVV) { Log.v(Constants.TAG, "Service onCreate"); } if (mSystemFacade == null) { mSystemFacade = new RealSystemFacade(this); } mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); mStorageManager = new StorageManager(this); mUpdateThread = new HandlerThread(TAG + "-UpdateThread"); mUpdateThread.start(); mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback); mScanner = new TransferScanner(this); mNotifier = new TransferNotifier(this); mNotifier.cancelAll(); mObserver = new TransferManagerContentObserver(); getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, true, mObserver); }
/** * Removes the local copy of the info about a transfer. */ private void deleteTransferLocked(long id) { TransferInfo info = mTransfers.get(id); if (info.mStatus == Downloads.Impl.STATUS_RUNNING) { info.mStatus = Downloads.Impl.STATUS_CANCELED; } if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) { if (Constants.LOGVV) { Log.d(TAG, "deleteTransferLocked() deleting " + info.mFileName); } deleteFileIfExists(info.mFileName); } mTransfers.remove(info.mId); }
/** * Build tag used for collapsing several {@link TransferInfo} into a single * {@link Notification}. */ private static String buildNotificationTag(TransferInfo info) { if (info.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) { return TYPE_WAITING + ":" + info.mPackage; } else if (isActiveAndVisible(info)) { return TYPE_ACTIVE + ":" + info.mPackage; } else if (isCompleteAndVisible(info)) { // Complete transfers always have unique notifs return TYPE_COMPLETE + ":" + info.mId; } else { return null; } }
/** * Prepare the destination file to receive data. If the file already exists, we'll set up * appropriately for resumption. */ private void setupDestinationFile(State state) throws StopRequestException { if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this transfer if (Constants.LOGV) { Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId + ", and state.mFilename: " + state.mFilename); } if (!Helpers.isFilenameValid(state.mFilename, mStorageManager.getTransferDataDirectory())) { // this should never happen throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, "found invalid internal destination filename"); } // We're resuming a transfer that got interrupted File f = new File(state.mFilename); if (f.exists()) { if (Constants.LOGV) { Log.i(Constants.TAG, "resuming transfer for id: " + mInfo.mId + ", and state.mFilename: " + state.mFilename); } long fileLength = f.length(); if (fileLength == 0) { // The transfer hadn't actually started, we can restart from scratch if (Constants.LOGVV) { Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting " + state.mFilename); } f.delete(); state.mFilename = null; if (Constants.LOGV) { Log.i(Constants.TAG, "resuming transfer for id: " + mInfo.mId + ", BUT starting from scratch again: "); } } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) { // This should've been caught upon failure if (Constants.LOGVV) { Log.d(TAG, "setupDestinationFile() unable to resume transfer, deleting " + state.mFilename); } f.delete(); throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, "Trying to resume a transfer that can't be resumed"); } else { // All right, we'll be able to resume this transfer if (Constants.LOGV) { Log.i(Constants.TAG, "resuming transfer for id: " + mInfo.mId + ", and starting with file of length: " + fileLength); } state.mCurrentBytes = (int) fileLength; if (mInfo.mTotalBytes != -1) { state.mContentLength = mInfo.mTotalBytes; } state.mHeaderETag = mInfo.mETag; state.mContinuingTransfer = true; if (Constants.LOGV) { Log.i(Constants.TAG, "resuming transfer for id: " + mInfo.mId + ", state.mCurrentBytes: " + state.mCurrentBytes + ", and setting mContinuingTransfer to true: "); } } } } }