diff options
-rw-r--r-- | Android.mk | 2 | ||||
-rw-r--r-- | src/com/android/providers/downloads/DownloadProvider.java | 35 | ||||
-rw-r--r-- | src/com/android/providers/downloads/DownloadStorageProvider.java | 131 | ||||
-rw-r--r-- | src/com/android/providers/downloads/DownloadThread.java | 6 | ||||
-rw-r--r-- | ui/res/values-kn-rIN/strings.xml | 2 |
5 files changed, 139 insertions, 37 deletions
@@ -11,6 +11,8 @@ LOCAL_PRIVILEGED_MODULE := true LOCAL_STATIC_JAVA_LIBRARIES := guava \ android-support-documents-archive +LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.providers.downloads.* + include $(BUILD_PACKAGE) # build UI + tests diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java index 4a64743e..b2b2c08d 100644 --- a/src/com/android/providers/downloads/DownloadProvider.java +++ b/src/com/android/providers/downloads/DownloadProvider.java @@ -18,7 +18,6 @@ package com.android.providers.downloads; import static android.provider.BaseColumns._ID; import static android.provider.Downloads.Impl.COLUMN_DESTINATION; -import static android.provider.Downloads.Impl.COLUMN_MEDIAPROVIDER_URI; import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED; import static android.provider.Downloads.Impl.COLUMN_MIME_TYPE; import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD; @@ -29,6 +28,7 @@ import android.app.DownloadManager; import android.app.DownloadManager.Request; import android.app.job.JobScheduler; import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; @@ -1207,7 +1207,10 @@ public final class DownloadProvider extends ContentProvider { Helpers.validateSelection(where, sAppReadableColumnsSet); } - final JobScheduler scheduler = getContext().getSystemService(JobScheduler.class); + final Context context = getContext(); + final ContentResolver resolver = context.getContentResolver(); + final JobScheduler scheduler = context.getSystemService(JobScheduler.class); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; int match = sURIMatcher.match(uri); @@ -1219,17 +1222,18 @@ public final class DownloadProvider extends ContentProvider { final SqlSelection selection = getWhereClause(uri, where, whereArgs, match); deleteRequestHeaders(db, selection.getSelection(), selection.getParameters()); - try (Cursor cursor = db.query(DB_TABLE, new String[] { - _ID, _DATA, COLUMN_MEDIAPROVIDER_URI - }, selection.getSelection(), selection.getParameters(), null, null, null)) { + try (Cursor cursor = db.query(DB_TABLE, null, selection.getSelection(), + selection.getParameters(), null, null, null)) { + final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); + final DownloadInfo info = new DownloadInfo(context); while (cursor.moveToNext()) { - final long id = cursor.getLong(0); - scheduler.cancel((int) id); + reader.updateFromDatabase(info); + scheduler.cancel((int) info.mId); - revokeAllDownloadsPermission(id); - DownloadStorageProvider.onDownloadProviderDelete(getContext(), id); + revokeAllDownloadsPermission(info.mId); + DownloadStorageProvider.onDownloadProviderDelete(getContext(), info.mId); - final String path = cursor.getString(1); + final String path = info.mFileName; if (!TextUtils.isEmpty(path)) { try { final File file = new File(path).getCanonicalFile(); @@ -1242,7 +1246,7 @@ public final class DownloadProvider extends ContentProvider { } } - final String mediaUri = cursor.getString(2); + final String mediaUri = info.mMediaProviderUri; if (!TextUtils.isEmpty(mediaUri)) { final long token = Binder.clearCallingIdentity(); try { @@ -1252,6 +1256,9 @@ public final class DownloadProvider extends ContentProvider { Binder.restoreCallingIdentity(token); } } + + // Tell requester that download is finished + info.sendIntentIfRequested(); } } @@ -1263,6 +1270,12 @@ public final class DownloadProvider extends ContentProvider { throw new UnsupportedOperationException("Cannot delete URI: " + uri); } notifyContentChanged(uri, match); + final long token = Binder.clearCallingIdentity(); + try { + Helpers.getDownloadNotifier(getContext()).update(); + } finally { + Binder.restoreCallingIdentity(token); + } return count; } diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java index 80d6ac79..4ec8e2d1 100644 --- a/src/com/android/providers/downloads/DownloadStorageProvider.java +++ b/src/com/android/providers/downloads/DownloadStorageProvider.java @@ -29,28 +29,36 @@ import android.net.Uri; import android.os.Binder; import android.os.CancellationSignal; import android.os.Environment; +import android.os.FileObserver; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; +import android.provider.Downloads; import android.support.provider.DocumentArchiveHelper; import android.text.TextUtils; -import android.webkit.MimeTypeMap; - -import libcore.io.IoUtils; +import android.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.text.NumberFormat; +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +import libcore.io.IoUtils; + /** * Presents a {@link DocumentsContract} view of {@link DownloadManager} * contents. */ public class DownloadStorageProvider extends DocumentsProvider { + private static final String TAG = "DownloadStorageProvider"; + private static final boolean DEBUG = false; + private static final String AUTHORITY = Constants.STORAGE_AUTHORITY; private static final String DOC_ID_ROOT = Constants.STORAGE_ROOT_ID; @@ -74,6 +82,7 @@ public class DownloadStorageProvider extends DocumentsProvider { mDm.setAccessAllDownloads(true); mDm.setAccessFilename(true); mArchiveHelper = new DocumentArchiveHelper(this, ':'); + return true; } @@ -179,7 +188,8 @@ public class DownloadStorageProvider extends DocumentsProvider { return mArchiveHelper.queryDocument(docId, projection); } - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + final DownloadsCursor result = + new DownloadsCursor(projection, getContext().getContentResolver()); if (DOC_ID_ROOT.equals(docId)) { includeDefaultDocument(result); @@ -200,6 +210,8 @@ public class DownloadStorageProvider extends DocumentsProvider { Binder.restoreCallingIdentity(token); } } + + result.start(); return result; } @@ -211,7 +223,8 @@ public class DownloadStorageProvider extends DocumentsProvider { return mArchiveHelper.queryChildDocuments(docId, projection, sortOrder); } - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + final DownloadsCursor result = + new DownloadsCursor(projection, getContext().getContentResolver()); // Delegate to real provider final long token = Binder.clearCallingIdentity(); @@ -227,6 +240,8 @@ public class DownloadStorageProvider extends DocumentsProvider { IoUtils.closeQuietly(cursor); Binder.restoreCallingIdentity(token); } + + result.start(); return result; } @@ -238,7 +253,8 @@ public class DownloadStorageProvider extends DocumentsProvider { return mArchiveHelper.queryDocument(parentDocumentId, projection); } - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + final DownloadsCursor result = + new DownloadsCursor(projection, getContext().getContentResolver()); // Delegate to real provider final long token = Binder.clearCallingIdentity(); @@ -254,13 +270,17 @@ public class DownloadStorageProvider extends DocumentsProvider { IoUtils.closeQuietly(cursor); Binder.restoreCallingIdentity(token); } + + result.start(); return result; } @Override public Cursor queryRecentDocuments(String rootId, String[] projection) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + + final DownloadsCursor result = + new DownloadsCursor(projection, getContext().getContentResolver()); // Delegate to real provider final long token = Binder.clearCallingIdentity(); @@ -288,6 +308,8 @@ public class DownloadStorageProvider extends DocumentsProvider { IoUtils.closeQuietly(cursor); Binder.restoreCallingIdentity(token); } + + result.start(); return result; } @@ -418,30 +440,91 @@ public class DownloadStorageProvider extends DocumentsProvider { } /** - * Remove file extension from name, but only if exact MIME type mapping - * exists. This means we can reapply the extension later. + * A MatrixCursor that spins up a file observer when the first instance is + * started ({@link #start()}, and stops the file observer when the last instance + * closed ({@link #close()}. When file changes are observed, a content change + * notification is sent on the Downloads content URI. + * + * <p>This is necessary as other processes, like ExternalStorageProvider, + * can access and modify files directly (without sending operations + * through DownloadStorageProvider). + * + * <p>Without this, contents accessible by one a Downloads cursor instance + * (like the Downloads root in Files app) can become state. */ - private static String removeExtension(String mimeType, String name) { - final int lastDot = name.lastIndexOf('.'); - if (lastDot >= 0) { - final String extension = name.substring(lastDot + 1); - final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - if (mimeType.equals(nameMime)) { - return name.substring(0, lastDot); + private static final class DownloadsCursor extends MatrixCursor { + + private static final Object mLock = new Object(); + @GuardedBy("mLock") + private static int mOpenCursorCount = 0; + @GuardedBy("mLock") + private static @Nullable ContentChangedRelay mFileWatcher; + + private final ContentResolver mResolver; + + DownloadsCursor(String[] projection, ContentResolver resolver) { + super(resolveDocumentProjection(projection)); + mResolver = resolver; + } + + void start() { + synchronized (mLock) { + if (mOpenCursorCount++ == 0) { + mFileWatcher = new ContentChangedRelay(mResolver); + mFileWatcher.startWatching(); + } + } + } + + @Override + public void close() { + super.close(); + synchronized (mLock) { + if (--mOpenCursorCount == 0) { + mFileWatcher.stopWatching(); + mFileWatcher = null; + } } } - return name; } /** - * Add file extension to name, but only if exact MIME type mapping exists. + * A file observer that notifies on the Downloads content URI(s) when + * files change on disk. */ - private static String addExtension(String mimeType, String name) { - final String extension = MimeTypeMap.getSingleton() - .getExtensionFromMimeType(mimeType); - if (extension != null) { - return name + "." + extension; + private static class ContentChangedRelay extends FileObserver { + private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO + | CREATE | DELETE | DELETE_SELF | MOVE_SELF; + + private static final String DOWNLOADS_PATH = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + .getAbsolutePath(); + private final ContentResolver mResolver; + + public ContentChangedRelay(ContentResolver resolver) { + super(DOWNLOADS_PATH, NOTIFY_EVENTS); + mResolver = resolver; + } + + @Override + public void startWatching() { + super.startWatching(); + if (DEBUG) Log.d(TAG, "Started watching for file changes in: " + DOWNLOADS_PATH); + } + + @Override + public void stopWatching() { + super.stopWatching(); + if (DEBUG) Log.d(TAG, "Stopped watching for file changes in: " + DOWNLOADS_PATH); + } + + @Override + public void onEvent(int event, String path) { + if ((event & NOTIFY_EVENTS) != 0) { + if (DEBUG) Log.v(TAG, "Change detected at path: " + DOWNLOADS_PATH); + mResolver.notifyChange(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, false); + mResolver.notifyChange(Downloads.Impl.CONTENT_URI, null, false); + } } - return name; } } diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java index 40194038..33436f57 100644 --- a/src/com/android/providers/downloads/DownloadThread.java +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -382,7 +382,11 @@ public class DownloadThread extends Thread { } if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) { - mInfo.sendIntentIfRequested(); + // If download was canceled, we already sent requested intent when + // deleted in the provider + if (mInfoDelta.mStatus != STATUS_CANCELED) { + mInfo.sendIntentIfRequested(); + } if (mInfo.shouldScanFile(mInfoDelta.mStatus)) { DownloadScanner.requestScanBlocking(mContext, mInfo.mId, mInfoDelta.mFileName, mInfoDelta.mMimeType); diff --git a/ui/res/values-kn-rIN/strings.xml b/ui/res/values-kn-rIN/strings.xml index c125612d..67722747 100644 --- a/ui/res/values-kn-rIN/strings.xml +++ b/ui/res/values-kn-rIN/strings.xml @@ -20,7 +20,7 @@ <string name="download_title_sorted_by_date" msgid="5898014492155434221">"ಡೌನ್ಲೋಡ್ಗಳು - ದಿನಾಂಕದ ಪ್ರಕಾರ ವಿಂಗಡಿಸಲಾಗಿದೆ"</string> <string name="download_title_sorted_by_size" msgid="1417193166677094813">"ಡೌನ್ಲೊಡ್ಗಳು - ಗಾತ್ರದ ಪ್ರಕಾರ ವಿಂಗಡಿಸಲಾಗಿದೆ"</string> <string name="no_downloads" msgid="1029667411186146836">"ಯಾವುದೇ ಡೌನ್ಲೋಡ್ಗಳಿಲ್ಲ."</string> - <string name="missing_title" msgid="830115697868833773">"<ಅಪರಿಚಿತ>"</string> + <string name="missing_title" msgid="830115697868833773">"<ಅಜ್ಞಾತ>"</string> <string name="button_sort_by_size" msgid="7331549713691146251">"ಗಾತ್ರದಂತೆ ವಿಂಗಡಿಸಿ"</string> <string name="button_sort_by_date" msgid="8800842892684101528">"ದಿನಾಂಕದ ಪ್ರಕಾರವಾಗಿ ವಿಂಗಡಿಸಿ"</string> <string name="download_queued" msgid="104973307780629904">"ಸರದಿಯಲ್ಲಿರಿಸಲಾಗಿದೆ"</string> |