diff options
-rw-r--r-- | Android.mk | 2 | ||||
-rw-r--r-- | src/com/android/providers/downloads/DownloadProvider.java | 66 | ||||
-rw-r--r-- | src/com/android/providers/downloads/DownloadStorageProvider.java | 131 | ||||
-rw-r--r-- | src/com/android/providers/downloads/DownloadThread.java | 1 | ||||
-rw-r--r-- | src/com/android/providers/downloads/OpenHelper.java | 17 | ||||
-rw-r--r-- | ui/res/values-kn-rIN/strings.xml | 2 |
6 files changed, 161 insertions, 58 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 4a0252b4..121ace46 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.COLUMN_OTHER_UID; @@ -31,6 +30,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; @@ -1055,10 +1055,14 @@ public final class DownloadProvider extends ContentProvider { Helpers.validateSelection(where, sAppReadableColumnsSet); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final Context context = getContext(); + final ContentResolver resolver = context.getContentResolver(); + + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; boolean updateSchedule = false; + boolean isCompleting = false; ContentValues filteredValues; if (Binder.getCallingPid() != Process.myPid()) { @@ -1099,6 +1103,7 @@ public final class DownloadProvider extends ContentProvider { if (isRestart || isUserBypassingSizeLimit) { updateSchedule = true; } + isCompleting = status != null && Downloads.Impl.isStatusCompleted(status); } int match = sURIMatcher.match(uri); @@ -1114,13 +1119,20 @@ public final class DownloadProvider extends ContentProvider { final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); count = qb.update(db, filteredValues, where, whereArgs); - if (updateSchedule) { + if (updateSchedule || isCompleting) { final long token = Binder.clearCallingIdentity(); - try { - try (Cursor cursor = qb.query(db, null, where, whereArgs, - null, null, null)) { - while (cursor.moveToNext()) { - Helpers.scheduleJob(getContext(), cursor.getInt(0)); + try (Cursor cursor = qb.query(db, null, where, whereArgs, + null, null, null)) { + final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, + cursor); + final DownloadInfo info = new DownloadInfo(context); + while (cursor.moveToNext()) { + reader.updateFromDatabase(info); + if (updateSchedule) { + Helpers.scheduleJob(context, info); + } + if (isCompleting) { + info.sendIntentIfRequested(); } } } finally { @@ -1225,7 +1237,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); @@ -1235,17 +1250,17 @@ public final class DownloadProvider extends ContentProvider { case ALL_DOWNLOADS: case ALL_DOWNLOADS_ID: final SQLiteQueryBuilder qb = getQueryBuilder(uri, match); - try (Cursor cursor = qb.query(db, new String[] { - _ID, _DATA, COLUMN_MEDIAPROVIDER_URI - }, where, whereArgs, null, null, null)) { + try (Cursor cursor = qb.query(db, null, where, whereArgs, 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(); @@ -1258,7 +1273,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 { @@ -1269,10 +1284,17 @@ public final class DownloadProvider extends ContentProvider { } } + // If the download wasn't completed yet, we're + // effectively completing it now, and we need to send + // any requested broadcasts + if (!Downloads.Impl.isStatusCompleted(info.mStatus)) { + info.sendIntentIfRequested(); + } + // Delete any headers for this download db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=?", - new String[] { Long.toString(id) }); + new String[] { Long.toString(info.mId) }); } } @@ -1284,6 +1306,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..c6b4c71a 100644 --- a/src/com/android/providers/downloads/DownloadThread.java +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -382,7 +382,6 @@ public class DownloadThread extends Thread { } if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) { - mInfo.sendIntentIfRequested(); if (mInfo.shouldScanFile(mInfoDelta.mStatus)) { DownloadScanner.requestScanBlocking(mContext, mInfo.mId, mInfoDelta.mFileName, mInfoDelta.mMimeType); diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java index 27ab86b9..69a44922 100644 --- a/src/com/android/providers/downloads/OpenHelper.java +++ b/src/com/android/providers/downloads/OpenHelper.java @@ -17,10 +17,10 @@ package com.android.providers.downloads; import static android.app.DownloadManager.COLUMN_LOCAL_FILENAME; -import static android.app.DownloadManager.COLUMN_LOCAL_URI; import static android.app.DownloadManager.COLUMN_MEDIA_TYPE; import static android.app.DownloadManager.COLUMN_URI; import static android.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI; + import static com.android.providers.downloads.Constants.TAG; import android.app.DownloadManager; @@ -30,7 +30,6 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.StrictMode; import android.provider.DocumentsContract; import android.provider.Downloads.Impl.RequestHeaders; import android.util.Log; @@ -51,14 +50,11 @@ public class OpenHelper { intent.addFlags(intentFlags); try { - StrictMode.disableDeathOnFileUriExposure(); context.startActivity(intent); return true; } catch (ActivityNotFoundException e) { Log.w(TAG, "Failed to start " + intent + ": " + e); return false; - } finally { - StrictMode.enableDeathOnFileUriExposure(); } } @@ -78,7 +74,6 @@ public class OpenHelper { return null; } - final Uri localUri = getCursorUri(cursor, COLUMN_LOCAL_URI); final File file = getCursorFile(cursor, COLUMN_LOCAL_FILENAME); String mimeType = getCursorString(cursor, COLUMN_MEDIA_TYPE); mimeType = DownloadDrmHelper.getOriginalMimeType(context, file, mimeType); @@ -87,20 +82,16 @@ public class OpenHelper { Constants.STORAGE_AUTHORITY, String.valueOf(id)); final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(documentUri, mimeType); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); if ("application/vnd.android.package-archive".equals(mimeType)) { - // PackageInstaller doesn't like content URIs, so open file - intent.setDataAndType(localUri, mimeType); - // Also splice in details about where it came from final Uri remoteUri = getCursorUri(cursor, COLUMN_URI); intent.putExtra(Intent.EXTRA_ORIGINATING_URI, remoteUri); intent.putExtra(Intent.EXTRA_REFERRER, getRefererUri(context, id)); intent.putExtra(Intent.EXTRA_ORIGINATING_UID, getOriginatingUid(context, id)); - } else { - intent.setDataAndType(documentUri, mimeType); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } return intent; 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> |