FileDocCategorySizeDatePackage
DownloadService.javaAPI DocAndroid 1.5 API38197Wed May 06 22:42:48 BST 2009com.android.providers.downloads

DownloadService

public class DownloadService extends android.app.Service
Performs the background downloads requested by applications that use the Downloads provider.

Fields Summary
private DownloadManagerContentObserver
mObserver
Observer to get notified when the content observer's data changes
private DownloadNotification
mNotifier
Class to handle Notification Manager updates
private ArrayList
mDownloads
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 UpdateThread
updateThread
The thread that updates the internal download list from the content provider.
private boolean
pendingUpdate
Whether the internal download list should be updated from the content provider.
private MediaScannerConnection
mMediaScannerConnection
The ServiceConnection object that tells us when we're connected to and disconnected from the Media Scanner
private boolean
mMediaScannerConnecting
private android.media.IMediaScannerService
mMediaScannerService
The IPC interface to the Media Scanner
private android.database.CharArrayBuffer
oldChars
Array used when extracting strings from content provider
private android.database.CharArrayBuffer
newChars
Array used when extracting strings from content provider
Constructors Summary
Methods Summary
private voiddeleteDownload(int arrayPos)
Removes the local copy of the info about a download.

        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);
    
private voidinsertDownload(android.database.Cursor cursor, int arrayPos, boolean networkAvailable, boolean networkRoaming, long now)
Keeps a local copy of the info about a download, and initiates the download if appropriate.

        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
        int retryRedirect =
                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER___REDIRECT_COUNT));
        DownloadInfo info = new DownloadInfo(
                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID)),
                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.URI)),
                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1,
                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME_HINT)),
                cursor.getString(cursor.getColumnIndexOrThrow(Downloads._DATA)),
                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.MIMETYPE)),
                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION)),
                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY)),
                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL)),
                cursor.getInt(statusColumn),
                cursor.getInt(failedColumn),
                retryRedirect & 0xfffffff,
                retryRedirect >> 28,
                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(Constants.ETAG)),
                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1);

        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "Service adding new entry");
            Log.v(Constants.TAG, "ID      : " + info.id);
            Log.v(Constants.TAG, "URI     : " + ((info.uri != null) ? "yes" : "no"));
            Log.v(Constants.TAG, "NO_INTEG: " + info.noIntegrity);
            Log.v(Constants.TAG, "HINT    : " + info.hint);
            Log.v(Constants.TAG, "FILENAME: " + info.filename);
            Log.v(Constants.TAG, "MIMETYPE: " + info.mimetype);
            Log.v(Constants.TAG, "DESTINAT: " + info.destination);
            Log.v(Constants.TAG, "VISIBILI: " + info.visibility);
            Log.v(Constants.TAG, "CONTROL : " + info.control);
            Log.v(Constants.TAG, "STATUS  : " + info.status);
            Log.v(Constants.TAG, "FAILED_C: " + info.numFailed);
            Log.v(Constants.TAG, "RETRY_AF: " + info.retryAfter);
            Log.v(Constants.TAG, "REDIRECT: " + info.redirectCount);
            Log.v(Constants.TAG, "LAST_MOD: " + info.lastMod);
            Log.v(Constants.TAG, "PACKAGE : " + info.pckg);
            Log.v(Constants.TAG, "CLASS   : " + info.clazz);
            Log.v(Constants.TAG, "COOKIES : " + ((info.cookies != null) ? "yes" : "no"));
            Log.v(Constants.TAG, "AGENT   : " + info.userAgent);
            Log.v(Constants.TAG, "REFERER : " + ((info.referer != null) ? "yes" : "no"));
            Log.v(Constants.TAG, "TOTAL   : " + info.totalBytes);
            Log.v(Constants.TAG, "CURRENT : " + info.currentBytes);
            Log.v(Constants.TAG, "ETAG    : " + info.etag);
            Log.v(Constants.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);
            ResolveInfo ri = getPackageManager().resolveActivity(mimetypeIntent,
                    PackageManager.MATCH_DEFAULT_ONLY);
            //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
            
            if (ri == null) {
                if (Config.LOGD) {
                    Log.d(Constants.TAG, "no application to handle MIME type " + info.mimetype);
                }
                info.status = Downloads.STATUS_NOT_ACCEPTABLE;

                Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id);
                ContentValues values = new ContentValues();
                values.put(Downloads.STATUS, Downloads.STATUS_NOT_ACCEPTABLE);
                getContentResolver().update(uri, values, null, null);
                info.sendIntentIfRequested(uri, this);
                return;
            }
        }

        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
            if (info.isReadyToStart(now)) {
                if (Constants.LOGV) {
                    Log.v(Constants.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;
                Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id);
                ContentValues values = new ContentValues();
                values.put(Downloads.STATUS, Downloads.STATUS_RUNNING_PAUSED);
                getContentResolver().update(uri, values, null, null);
            }
        }
    
private booleanmediaScannerConnected()
Returns whether we have a live connection to the Media Scanner

        return mMediaScannerService != null;
    
private longnextAction(int arrayPos, long now)
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"

        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;
    
public android.os.IBinderonBind(android.content.Intent i)
Returns an IBinder instance when someone wants to connect to this service. Binding to this service is not allowed.

throws
UnsupportedOperationException

        throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
    
public voidonCreate()
Initializes the service when it is first created

        super.onCreate();
        if (Constants.LOGVV) {
            Log.v(Constants.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();
    
public voidonDestroy()
Cleans up when the service is destroyed

        getContentResolver().unregisterContentObserver(mObserver);
        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "Service onDestroy");
        }
        super.onDestroy();
    
public voidonStart(android.content.Intent intent, int startId)
Responds to a call to startService

        super.onStart(intent, startId);
        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "Service onStart");
        }

        updateFromProvider();
    
private voidremoveSpuriousFiles()
Removes files that may have been left behind in the cache directory

        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._DATA }, 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();
        }
    
private booleanscanFile(android.database.Cursor cursor, int arrayPos)
Attempts to scan the file if necessary. Returns true if the file has been properly scanned.

        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
        synchronized (this) {
            if (mMediaScannerService != null) {
                try {
                    if (Constants.LOGV) {
                        Log.v(Constants.TAG, "Scanning file " + info.filename);
                    }
                    mMediaScannerService.scanFile(info.filename, info.mimetype);
                    if (cursor != null) {
                        ContentValues values = new ContentValues();
                        values.put(Constants.MEDIA_SCANNED, 1);
                        getContentResolver().update(
                                ContentUris.withAppendedId(Downloads.CONTENT_URI,
                                       cursor.getLong(cursor.getColumnIndexOrThrow(Downloads._ID))),
                                values, null, null);
                    }
                    return true;
                } catch (RemoteException e) {
                    if (Config.LOGD) {
                        Log.d(Constants.TAG, "Failed to scan file " + info.filename);
                    }
                }
            }
        }
        return false;
    
private booleanshouldScanFile(int arrayPos)
Returns whether a file should be scanned

        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);
    
private java.lang.StringstringFromCursor(java.lang.String old, android.database.Cursor cursor, java.lang.String column)
Returns a String that holds the current value of the column, optimizing for the case where the value hasn't changed.

        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;
    
private voidtrimDatabase()
Drops old rows from the database to prevent it from growing too large

        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(Constants.TAG, "null cursor in trimDatabase");
            return;
        }
        if (cursor.moveToFirst()) {
            int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
            int columnId = cursor.getColumnIndexOrThrow(Downloads._ID);
            while (numDelete > 0) {
                getContentResolver().delete(
                        ContentUris.withAppendedId(Downloads.CONTENT_URI, cursor.getLong(columnId)),
                        null, null);
                if (!cursor.moveToNext()) {
                    break;
                }
                numDelete--;
            }
        }
        cursor.close();
    
private voidupdateDownload(android.database.Cursor cursor, int arrayPos, boolean networkAvailable, boolean networkRoaming, long now)
Updates the local copy of the info about a download.

        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
        info.id = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID));
        info.uri = stringFromCursor(info.uri, cursor, Downloads.URI);
        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._DATA);
        info.mimetype = stringFromCursor(info.mimetype, cursor, Downloads.MIMETYPE);
        info.destination = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION));
        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;
        synchronized(info) {
            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);
        int retryRedirect =
                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER___REDIRECT_COUNT));
        info.retryAfter = retryRedirect & 0xfffffff;
        info.redirectCount = retryRedirect >> 28;
        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, Constants.ETAG);
        info.mediaScanned =
                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;

        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
            if (info.isReadyToRestart(now)) {
                if (Constants.LOGV) {
                    Log.v(Constants.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();
            }
        }
    
private voidupdateFromProvider()
Parses data from the content provider into private array

        synchronized (this) {
            pendingUpdate = true;
            if (updateThread == null) {
                updateThread = new UpdateThread();
                updateThread.start();
            }
        }
    
private booleanvisibleNotification(int arrayPos)
Returns whether there's a visible notification for this download

        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
        return info.hasCompletionNotification();