summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/DownloadService.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads/DownloadService.java')
-rw-r--r--src/com/android/providers/downloads/DownloadService.java859
1 files changed, 859 insertions, 0 deletions
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
new file mode 100644
index 00000000..0d3650c0
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -0,0 +1,859 @@
+/*
+ * 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.downloads;
+
+import com.google.android.collect.Lists;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.ServiceConnection;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.CharArrayBuffer;
+import android.drm.mobile1.DrmRawContent;
+import android.media.IMediaScannerService;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Process;
+import android.provider.BaseColumns;
+import android.provider.Downloads;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Performs the background downloads requested by applications that use the Downloads provider.
+ */
+public class DownloadService extends Service {
+
+ /* ------------ Constants ------------ */
+
+ /** Tag used for debugging/logging */
+ private static final String TAG = Constants.TAG;
+
+ /* ------------ Members ------------ */
+
+ /** Observer to get notified when the content observer's data changes */
+ private DownloadManagerContentObserver mObserver;
+
+ /** Class to handle Notification Manager updates */
+ private DownloadNotification mNotifier;
+
+ /**
+ * The Service's view of the list of downloads. This is kept independently
+ * from the content provider, and the Service only initiates downloads
+ * based on this data, so that it can deal with situation where the data
+ * in the content provider changes or disappears.
+ */
+ private ArrayList<DownloadInfo> mDownloads;
+
+ /**
+ * The thread that updates the internal download list from the content
+ * provider.
+ */
+ private UpdateThread updateThread;
+
+ /**
+ * Whether the internal download list should be updated from the content
+ * provider.
+ */
+ private boolean pendingUpdate;
+
+ /**
+ * The ServiceConnection object that tells us when we're connected to and disconnected from
+ * the Media Scanner
+ */
+ private MediaScannerConnection mMediaScannerConnection;
+
+ private boolean mMediaScannerConnecting;
+
+ /**
+ * The IPC interface to the Media Scanner
+ */
+ private IMediaScannerService mMediaScannerService;
+
+ /**
+ * Array used when extracting strings from content provider
+ */
+ private CharArrayBuffer oldChars;
+
+ /**
+ * Array used when extracting strings from content provider
+ */
+ private CharArrayBuffer newChars;
+
+ /* ------------ Inner Classes ------------ */
+
+ /**
+ * Receives notifications when the data in the content provider changes
+ */
+ private class DownloadManagerContentObserver extends ContentObserver {
+
+ public DownloadManagerContentObserver() {
+ super(new Handler());
+ }
+
+ /**
+ * Receives notification when the data in the observed content
+ * provider changes.
+ */
+ public void onChange(final boolean selfChange) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service ContentObserver received notification");
+ }
+ updateFromProvider();
+ }
+
+ }
+
+ /**
+ * Gets called back when the connection to the media
+ * scanner is established or lost.
+ */
+ public class MediaScannerConnection implements ServiceConnection {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Connected to Media Scanner");
+ }
+ mMediaScannerConnecting = false;
+ synchronized (DownloadService.this) {
+ mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
+ if (mMediaScannerService != null) {
+ updateFromProvider();
+ }
+ }
+ }
+
+ public void disconnectMediaScanner() {
+ synchronized (DownloadService.this) {
+ if (mMediaScannerService != null) {
+ mMediaScannerService = null;
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Disconnecting from Media Scanner");
+ }
+ try {
+ unbindService(this);
+ } catch (IllegalArgumentException ex) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "unbindService threw up: " + ex);
+ }
+ }
+ }
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Disconnected from Media Scanner");
+ }
+ synchronized (DownloadService.this) {
+ mMediaScannerService = null;
+ }
+ }
+ }
+
+ /* ------------ Methods ------------ */
+
+ /**
+ * Returns an IBinder instance when someone wants to connect to this
+ * service. Binding to this service is not allowed.
+ *
+ * @throws UnsupportedOperationException
+ */
+ public IBinder onBind(Intent i) {
+ throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
+ }
+
+ /**
+ * Initializes the service when it is first created
+ */
+ public void onCreate() {
+ super.onCreate();
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service onCreate");
+ }
+
+ mDownloads = Lists.newArrayList();
+
+ mObserver = new DownloadManagerContentObserver();
+ getContentResolver().registerContentObserver(Downloads.CONTENT_URI,
+ true, mObserver);
+
+ mMediaScannerService = null;
+ mMediaScannerConnecting = false;
+ mMediaScannerConnection = new MediaScannerConnection();
+
+ mNotifier = new DownloadNotification(this);
+ mNotifier.mNotificationMgr.cancelAll();
+ mNotifier.updateNotification();
+
+ trimDatabase();
+ removeSpuriousFiles();
+ updateFromProvider();
+ }
+
+ /**
+ * Responds to a call to startService
+ */
+ public void onStart(Intent intent, int startId) {
+ super.onStart(intent, startId);
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service onStart");
+ }
+
+ updateFromProvider();
+ }
+
+ /**
+ * Cleans up when the service is destroyed
+ */
+ public void onDestroy() {
+ getContentResolver().unregisterContentObserver(mObserver);
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service onDestroy");
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * Parses data from the content provider into private array
+ */
+ private void updateFromProvider() {
+ synchronized (this) {
+ pendingUpdate = true;
+ if (updateThread == null) {
+ updateThread = new UpdateThread();
+ updateThread.start();
+ }
+ }
+ }
+
+ private class UpdateThread extends Thread {
+ public UpdateThread() {
+ super("Download Service");
+ }
+
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ boolean keepService = false;
+ // for each update from the database, remember which download is
+ // supposed to get restarted soonest in the future
+ long wakeUp = Long.MAX_VALUE;
+ for (;;) {
+ synchronized (DownloadService.this) {
+ if (updateThread != this) {
+ throw new IllegalStateException(
+ "multiple UpdateThreads in DownloadService");
+ }
+ if (!pendingUpdate) {
+ updateThread = null;
+ if (!keepService) {
+ stopSelf();
+ }
+ if (wakeUp != Long.MAX_VALUE) {
+ AlarmManager alarms =
+ (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ if (alarms == null) {
+ Log.e(Constants.TAG, "couldn't get alarm manager");
+ } else {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
+ }
+ Intent intent = new Intent(Constants.ACTION_RETRY);
+ intent.setClassName("com.android.providers.downloads",
+ DownloadReceiver.class.getName());
+ alarms.set(
+ AlarmManager.RTC_WAKEUP,
+ System.currentTimeMillis() + wakeUp,
+ PendingIntent.getBroadcast(DownloadService.this, 0, intent,
+ PendingIntent.FLAG_ONE_SHOT));
+ }
+ }
+ oldChars = null;
+ newChars = null;
+ return;
+ }
+ pendingUpdate = false;
+ }
+ boolean networkAvailable = Helpers.isNetworkAvailable(DownloadService.this);
+ long now = System.currentTimeMillis();
+
+ Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
+ null, null, null, BaseColumns._ID);
+
+ if (cursor == null) {
+ return;
+ }
+
+ cursor.moveToFirst();
+
+ int arrayPos = 0;
+
+ boolean mustScan = false;
+ keepService = false;
+ wakeUp = Long.MAX_VALUE;
+
+ boolean isAfterLast = cursor.isAfterLast();
+
+ int idColumn = cursor.getColumnIndexOrThrow(BaseColumns._ID);
+
+ /*
+ * Walk the cursor and the local array to keep them in sync. The key
+ * to the algorithm is that the ids are unique and sorted both in
+ * the cursor and in the array, so that they can be processed in
+ * order in both sources at the same time: at each step, both
+ * sources point to the lowest id that hasn't been processed from
+ * that source, and the algorithm processes the lowest id from
+ * those two possibilities.
+ * At each step:
+ * -If the array contains an entry that's not in the cursor, remove the
+ * entry, move to next entry in the array.
+ * -If the array contains an entry that's in the cursor, nothing to do,
+ * move to next cursor row and next array entry.
+ * -If the cursor contains an entry that's not in the array, insert
+ * a new entry in the array, move to next cursor row and next
+ * array entry.
+ */
+ while (!isAfterLast || arrayPos < mDownloads.size()) {
+ if (isAfterLast) {
+ // We're beyond the end of the cursor but there's still some
+ // stuff in the local array, which can only be junk
+ if (Constants.LOGVV) {
+ int arrayId = ((DownloadInfo) mDownloads.get(arrayPos)).id;
+ Log.v(TAG, "Array update: trimming " + arrayId + " @ " + arrayPos);
+ }
+ if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
+ scanFile(null, arrayPos);
+ }
+ deleteDownload(arrayPos); // this advances in the array
+ } else {
+ int id = cursor.getInt(idColumn);
+
+ if (arrayPos == mDownloads.size()) {
+ insertDownload(cursor, arrayPos, networkAvailable, now);
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
+ }
+ if (shouldScanFile(arrayPos)
+ && (!mediaScannerConnected() || !scanFile(cursor, arrayPos))) {
+ mustScan = true;
+ keepService = true;
+ }
+ if (visibleNotification(arrayPos)) {
+ keepService = true;
+ }
+ long next = nextAction(arrayPos, now);
+ if (next == 0) {
+ keepService = true;
+ } else if (next > 0 && next < wakeUp) {
+ wakeUp = next;
+ }
+ ++arrayPos;
+ cursor.moveToNext();
+ isAfterLast = cursor.isAfterLast();
+ } else {
+ int arrayId = mDownloads.get(arrayPos).id;
+
+ if (arrayId < id) {
+ // The array entry isn't in the cursor
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Array update: removing " + arrayId
+ + " @ " + arrayPos);
+ }
+ if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
+ scanFile(null, arrayPos);
+ }
+ deleteDownload(arrayPos); // this advances in the array
+ } else if (arrayId == id) {
+ // This cursor row already exists in the stored array
+ updateDownload(cursor, arrayPos, networkAvailable, now);
+ if (shouldScanFile(arrayPos)
+ && (!mediaScannerConnected()
+ || !scanFile(cursor, arrayPos))) {
+ mustScan = true;
+ keepService = true;
+ }
+ if (visibleNotification(arrayPos)) {
+ keepService = true;
+ }
+ long next = nextAction(arrayPos, now);
+ if (next == 0) {
+ keepService = true;
+ } else if (next > 0 && next < wakeUp) {
+ wakeUp = next;
+ }
+ ++arrayPos;
+ cursor.moveToNext();
+ isAfterLast = cursor.isAfterLast();
+ } else {
+ // This cursor entry didn't exist in the stored array
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
+ }
+ insertDownload(cursor, arrayPos, networkAvailable, now);
+ if (shouldScanFile(arrayPos)
+ && (!mediaScannerConnected()
+ || !scanFile(cursor, arrayPos))) {
+ mustScan = true;
+ keepService = true;
+ }
+ if (visibleNotification(arrayPos)) {
+ keepService = true;
+ }
+ long next = nextAction(arrayPos, now);
+ if (next == 0) {
+ keepService = true;
+ } else if (next > 0 && next < wakeUp) {
+ wakeUp = next;
+ }
+ ++arrayPos;
+ cursor.moveToNext();
+ isAfterLast = cursor.isAfterLast();
+ }
+ }
+ }
+ }
+
+ mNotifier.updateNotification();
+
+ if (mustScan) {
+ if (!mMediaScannerConnecting) {
+ Intent intent = new Intent();
+ intent.setClassName("com.android.providers.media",
+ "com.android.providers.media.MediaScannerService");
+ mMediaScannerConnecting = true;
+ bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
+ }
+ } else {
+ mMediaScannerConnection.disconnectMediaScanner();
+ }
+
+ if (!cursor.commitUpdates()) {
+ Log.e(Constants.TAG, "commitUpdates failed in updateFromProvider");
+ }
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Removes files that may have been left behind in the cache directory
+ */
+ private void removeSpuriousFiles() {
+ File[] files = Environment.getDownloadCacheDirectory().listFiles();
+ if (files == null) {
+ // The cache folder doesn't appear to exist (this is likely the case
+ // when running the simulator).
+ return;
+ }
+ HashSet<String> fileSet = new HashSet();
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
+ continue;
+ }
+ if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
+ continue;
+ }
+ fileSet.add(files[i].getPath());
+ }
+
+ Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
+ new String[] { Downloads.FILENAME }, null, null, null);
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ do {
+ fileSet.remove(cursor.getString(0));
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ }
+ Iterator<String> iterator = fileSet.iterator();
+ while (iterator.hasNext()) {
+ String filename = iterator.next();
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "deleting spurious file " + filename);
+ }
+ new File(filename).delete();
+ }
+ }
+
+ /**
+ * Drops old rows from the database to prevent it from growing too large
+ */
+ private void trimDatabase() {
+ Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
+ new String[] { Downloads._ID },
+ Downloads.STATUS + " >= 200", null,
+ Downloads.LAST_MODIFICATION);
+ if (cursor == null) {
+ // This isn't good - if we can't do basic queries in our database, nothing's gonna work
+ Log.e(TAG, "null cursor in trimDatabase");
+ return;
+ }
+ if (cursor.moveToFirst()) {
+ while (cursor.getCount() > Constants.MAX_DOWNLOADS) {
+ cursor.deleteRow();
+ }
+ }
+ cursor.close();
+ }
+
+ /**
+ * Keeps a local copy of the info about a download, and initiates the
+ * download if appropriate.
+ */
+ private void insertDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
+ int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
+ int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
+ DownloadInfo info = new DownloadInfo(
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.URI)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ENTITY)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1,
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME_HINT)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1,
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.MIMETYPE)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL)),
+ cursor.getInt(statusColumn),
+ cursor.getInt(failedColumn),
+ cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_PACKAGE)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_CLASS)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_EXTRAS)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COOKIE_DATA)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.USER_AGENT)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.REFERER)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ETAG)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1);
+
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service adding new entry");
+ Log.v(TAG, "ID : " + info.id);
+ Log.v(TAG, "URI : " + ((info.uri != null) ? "yes" : "no"));
+ Log.v(TAG, "METHOD : " + info.method);
+ Log.v(TAG, "ENTITY : " + ((info.entity != null) ? "yes" : "no"));
+ Log.v(TAG, "NO_INTEG: " + info.noIntegrity);
+ Log.v(TAG, "HINT : " + info.hint);
+ Log.v(TAG, "FILENAME: " + info.filename);
+ Log.v(TAG, "SYSIMAGE: " + info.otaUpdate);
+ Log.v(TAG, "MIMETYPE: " + info.mimetype);
+ Log.v(TAG, "DESTINAT: " + info.destination);
+ Log.v(TAG, "NO_SYSTE: " + info.noSystem);
+ Log.v(TAG, "VISIBILI: " + info.visibility);
+ Log.v(TAG, "CONTROL : " + info.control);
+ Log.v(TAG, "STATUS : " + info.status);
+ Log.v(TAG, "FAILED_C: " + info.numFailed);
+ Log.v(TAG, "LAST_MOD: " + info.lastMod);
+ Log.v(TAG, "PACKAGE : " + info.pckg);
+ Log.v(TAG, "CLASS : " + info.clazz);
+ Log.v(TAG, "COOKIES : " + ((info.cookies != null) ? "yes" : "no"));
+ Log.v(TAG, "AGENT : " + info.userAgent);
+ Log.v(TAG, "REFERER : " + ((info.referer != null) ? "yes" : "no"));
+ Log.v(TAG, "TOTAL : " + info.totalBytes);
+ Log.v(TAG, "CURRENT : " + info.currentBytes);
+ Log.v(TAG, "ETAG : " + info.etag);
+ Log.v(TAG, "SCANNED : " + info.mediaScanned);
+ }
+
+ mDownloads.add(arrayPos, info);
+
+ if (info.status == 0
+ && (info.destination == Downloads.DESTINATION_EXTERNAL
+ || info.destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE)
+ && info.mimetype != null
+ && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mimetype)) {
+ // Check to see if we are allowed to download this file. Only files
+ // that can be handled by the platform can be downloaded.
+ // special case DRM files, which we should always allow downloading.
+ Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
+
+ // We can provide data as either content: or file: URIs,
+ // so allow both. (I think it would be nice if we just did
+ // everything as content: URIs)
+ // Actually, right now the download manager's UId restrictions
+ // prevent use from using content: so it's got to be file: or
+ // nothing
+
+ mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mimetype);
+ List<ResolveInfo> list = getPackageManager().queryIntentActivities(mimetypeIntent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ //Log.i(TAG, "*** QUERY " + mimetypeIntent + ": " + list);
+
+ if (list.size() == 0
+ || (info.noSystem && info.mimetype.equalsIgnoreCase(Constants.MIMETYPE_APK))) {
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "no application to handle MIME type " + info.mimetype);
+ }
+ info.status = Downloads.STATUS_NOT_ACCEPTABLE;
+ cursor.updateInt(statusColumn, Downloads.STATUS_NOT_ACCEPTABLE);
+
+ Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + info.id);
+ Intent intent = new Intent(Downloads.DOWNLOAD_COMPLETED_ACTION);
+ intent.setData(uri);
+ sendBroadcast(intent, "android.permission.ACCESS_DOWNLOAD_DATA");
+ info.sendIntentIfRequested(uri, this);
+ return;
+ }
+ }
+
+ if (networkAvailable) {
+ if (info.isReadyToStart(now)) {
+ if (Constants.LOGV) {
+ Log.v(TAG, "Service spawning thread to handle new download " + info.id);
+ }
+ if (info.hasActiveThread) {
+ throw new IllegalStateException("Multiple threads on same download on insert");
+ }
+ if (info.status != Downloads.STATUS_RUNNING) {
+ info.status = Downloads.STATUS_RUNNING;
+ ContentValues values = new ContentValues();
+ values.put(Downloads.STATUS, info.status);
+ getContentResolver().update(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id),
+ values, null, null);
+ }
+ DownloadThread downloader = new DownloadThread(this, info);
+ info.hasActiveThread = true;
+ downloader.start();
+ }
+ } else {
+ if (info.status == 0
+ || info.status == Downloads.STATUS_PENDING
+ || info.status == Downloads.STATUS_RUNNING) {
+ info.status = Downloads.STATUS_RUNNING_PAUSED;
+ cursor.updateInt(statusColumn, Downloads.STATUS_RUNNING_PAUSED);
+ }
+ }
+ }
+
+ /**
+ * Updates the local copy of the info about a download.
+ */
+ private void updateDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
+ int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
+ info.id = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID));
+ info.uri = stringFromCursor(info.uri, cursor, Downloads.URI);
+ info.method = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD));
+ info.entity = stringFromCursor(info.entity, cursor, Downloads.ENTITY);
+ info.noIntegrity =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1;
+ info.hint = stringFromCursor(info.hint, cursor, Downloads.FILENAME_HINT);
+ info.filename = stringFromCursor(info.filename, cursor, Downloads.FILENAME);
+ info.otaUpdate = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1;
+ info.mimetype = stringFromCursor(info.mimetype, cursor, Downloads.MIMETYPE);
+ info.destination = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION));
+ info.noSystem =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1;
+ int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY));
+ if (info.visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
+ && newVisibility != Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
+ && Downloads.isStatusCompleted(info.status)) {
+ mNotifier.mNotificationMgr.cancel(info.id);
+ }
+ info.visibility = newVisibility;
+ info.control = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL));
+ int newStatus = cursor.getInt(statusColumn);
+ if (!Downloads.isStatusCompleted(info.status) && Downloads.isStatusCompleted(newStatus)) {
+ mNotifier.mNotificationMgr.cancel(info.id);
+ }
+ info.status = newStatus;
+ info.numFailed = cursor.getInt(failedColumn);
+ info.lastMod = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION));
+ info.pckg = stringFromCursor(info.pckg, cursor, Downloads.NOTIFICATION_PACKAGE);
+ info.clazz = stringFromCursor(info.clazz, cursor, Downloads.NOTIFICATION_CLASS);
+ info.cookies = stringFromCursor(info.cookies, cursor, Downloads.COOKIE_DATA);
+ info.userAgent = stringFromCursor(info.userAgent, cursor, Downloads.USER_AGENT);
+ info.referer = stringFromCursor(info.referer, cursor, Downloads.REFERER);
+ info.totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES));
+ info.currentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES));
+ info.etag = stringFromCursor(info.etag, cursor, Downloads.ETAG);
+ info.mediaScanned =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1;
+
+ if (networkAvailable) {
+ if (info.isReadyToRestart(now)) {
+ if (Constants.LOGV) {
+ Log.v(TAG, "Service spawning thread to handle updated download " + info.id);
+ }
+ if (info.hasActiveThread) {
+ throw new IllegalStateException("Multiple threads on same download on update");
+ }
+ info.status = Downloads.STATUS_RUNNING;
+ ContentValues values = new ContentValues();
+ values.put(Downloads.STATUS, info.status);
+ getContentResolver().update(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id),
+ values, null, null);
+ DownloadThread downloader = new DownloadThread(this, info);
+ info.hasActiveThread = true;
+ downloader.start();
+ }
+ }
+ }
+
+ /**
+ * Returns a String that holds the current value of the column,
+ * optimizing for the case where the value hasn't changed.
+ */
+ private String stringFromCursor(String old, Cursor cursor, String column) {
+ int index = cursor.getColumnIndexOrThrow(column);
+ if (old == null) {
+ return cursor.getString(index);
+ }
+ if (newChars == null) {
+ newChars = new CharArrayBuffer(128);
+ }
+ cursor.copyStringToBuffer(index, newChars);
+ int length = newChars.sizeCopied;
+ if (length != old.length()) {
+ return cursor.getString(index);
+ }
+ if (oldChars == null || oldChars.sizeCopied < length) {
+ oldChars = new CharArrayBuffer(length);
+ }
+ char[] oldArray = oldChars.data;
+ char[] newArray = newChars.data;
+ old.getChars(0, length, oldArray, 0);
+ for (int i = length - 1; i >= 0; --i) {
+ if (oldArray[i] != newArray[i]) {
+ return new String(newArray, 0, length);
+ }
+ }
+ return old;
+ }
+
+ /**
+ * Removes the local copy of the info about a download.
+ */
+ private void deleteDownload(int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ if (info.status == Downloads.STATUS_RUNNING) {
+ info.status = Downloads.STATUS_CANCELED;
+ } else if (info.destination != Downloads.DESTINATION_EXTERNAL && info.filename != null) {
+ new File(info.filename).delete();
+ }
+ mNotifier.mNotificationMgr.cancel(info.id);
+
+ mDownloads.remove(arrayPos);
+ }
+
+ /**
+ * Returns the amount of time (as measured from the "now" parameter)
+ * at which a download will be active.
+ * 0 = immediately - service should stick around to handle this download.
+ * -1 = never - service can go away without ever waking up.
+ * positive value - service must wake up in the future, as specified in ms from "now"
+ */
+ private long nextAction(int arrayPos, long now) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ if (Downloads.isStatusCompleted(info.status)) {
+ return -1;
+ }
+ if (info.status != Downloads.STATUS_RUNNING_PAUSED) {
+ return 0;
+ }
+ if (info.numFailed == 0) {
+ return 0;
+ }
+ long when = info.restartTime();
+ if (when <= now) {
+ return 0;
+ }
+ return when - now;
+ }
+
+ /**
+ * Returns whether there's a visible notification for this download
+ */
+ private boolean visibleNotification(int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ return info.hasCompletionNotification();
+ }
+
+ /**
+ * Returns whether a file should be scanned
+ */
+ private boolean shouldScanFile(int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ return !info.mediaScanned
+ && info.destination == Downloads.DESTINATION_EXTERNAL
+ && Downloads.isStatusSuccess(info.status)
+ && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mimetype);
+ }
+
+ /**
+ * Returns whether we have a live connection to the Media Scanner
+ */
+ private boolean mediaScannerConnected() {
+ return mMediaScannerService != null;
+ }
+
+ /**
+ * Attempts to scan the file if necessary.
+ * Returns true if the file has been properly scanned.
+ */
+ private boolean scanFile(Cursor cursor, int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ synchronized (this) {
+ if (mMediaScannerService != null) {
+ try {
+ if (Constants.LOGV) {
+ Log.v(TAG, "Scanning file " + info.filename);
+ }
+ mMediaScannerService.scanFile(info.filename, info.mimetype);
+ if (cursor != null) {
+ cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED), 1);
+ }
+ return true;
+ } catch (RemoteException e) {
+ if (Config.LOGD) {
+ Log.d(TAG, "Failed to scan file " + info.filename);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+}