• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.downloads;
18 
19 import static com.android.providers.downloads.MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload;
20 import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreIdString;
21 import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreUri;
22 import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownload;
23 import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownloadDir;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.app.DownloadManager;
28 import android.app.DownloadManager.Query;
29 import android.content.ContentResolver;
30 import android.content.ContentUris;
31 import android.content.ContentValues;
32 import android.content.Context;
33 import android.content.UriPermission;
34 import android.database.Cursor;
35 import android.database.MatrixCursor;
36 import android.database.MatrixCursor.RowBuilder;
37 import android.media.MediaFile;
38 import android.net.Uri;
39 import android.os.Binder;
40 import android.os.Bundle;
41 import android.os.CancellationSignal;
42 import android.os.Environment;
43 import android.os.FileObserver;
44 import android.os.FileUtils;
45 import android.os.ParcelFileDescriptor;
46 import android.provider.DocumentsContract;
47 import android.provider.DocumentsContract.Document;
48 import android.provider.DocumentsContract.Path;
49 import android.provider.DocumentsContract.Root;
50 import android.provider.Downloads;
51 import android.provider.MediaStore;
52 import android.provider.MediaStore.DownloadColumns;
53 import android.text.TextUtils;
54 import android.util.Log;
55 import android.util.Pair;
56 
57 import com.android.internal.annotations.GuardedBy;
58 import com.android.internal.content.FileSystemProvider;
59 
60 import libcore.io.IoUtils;
61 
62 import java.io.File;
63 import java.io.FileNotFoundException;
64 import java.text.NumberFormat;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.Collections;
68 import java.util.HashSet;
69 import java.util.List;
70 import java.util.Set;
71 
72 /**
73  * Presents files located in {@link Environment#DIRECTORY_DOWNLOADS} and contents from
74  * {@link DownloadManager}. {@link DownloadManager} contents include active downloads and completed
75  * downloads added by other applications using
76  * {@link DownloadManager#addCompletedDownload(String, String, boolean, String, String, long, boolean, boolean, Uri, Uri)}
77  * .
78  */
79 public class DownloadStorageProvider extends FileSystemProvider {
80     private static final String TAG = "DownloadStorageProvider";
81     private static final boolean DEBUG = false;
82 
83     private static final String AUTHORITY = Constants.STORAGE_AUTHORITY;
84     private static final String DOC_ID_ROOT = Constants.STORAGE_ROOT_ID;
85 
86     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
87             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
88             Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_QUERY_ARGS
89     };
90 
91     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
92             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
93             Document.COLUMN_SUMMARY, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS,
94             Document.COLUMN_SIZE,
95     };
96 
97     private DownloadManager mDm;
98 
99     private static final int NO_LIMIT = -1;
100 
101     @Override
onCreate()102     public boolean onCreate() {
103         super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
104         mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
105         mDm.setAccessAllDownloads(true);
106         mDm.setAccessFilename(true);
107 
108         return true;
109     }
110 
resolveRootProjection(String[] projection)111     private static String[] resolveRootProjection(String[] projection) {
112         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
113     }
114 
resolveDocumentProjection(String[] projection)115     private static String[] resolveDocumentProjection(String[] projection) {
116         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
117     }
118 
copyNotificationUri(MatrixCursor result, Cursor cursor)119     private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
120         result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
121     }
122 
123     /**
124      * Called by {@link DownloadProvider} when deleting a row in the {@link DownloadManager}
125      * database.
126      */
onDownloadProviderDelete(Context context, long id)127     static void onDownloadProviderDelete(Context context, long id) {
128         final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, Long.toString(id));
129         context.revokeUriPermission(uri, ~0);
130     }
131 
onMediaProviderDownloadsDelete(Context context, long[] ids, String[] mimeTypes)132     static void onMediaProviderDownloadsDelete(Context context, long[] ids, String[] mimeTypes) {
133         for (int i = 0; i < ids.length; ++i) {
134             final boolean isDir = mimeTypes[i] == null;
135             final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY,
136                     MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload(ids[i], isDir));
137             context.revokeUriPermission(uri, ~0);
138         }
139     }
140 
revokeAllMediaStoreUriPermissions(Context context)141     static void revokeAllMediaStoreUriPermissions(Context context) {
142         final List<UriPermission> uriPermissions =
143                 context.getContentResolver().getOutgoingUriPermissions();
144         final int size = uriPermissions.size();
145         final StringBuilder sb = new StringBuilder("Revoking permissions for uris: ");
146         for (int i = 0; i < size; ++i) {
147             final Uri uri = uriPermissions.get(i).getUri();
148             if (AUTHORITY.equals(uri.getAuthority())
149                     && isMediaStoreDownload(DocumentsContract.getDocumentId(uri))) {
150                 context.revokeUriPermission(uri, ~0);
151                 sb.append(uri + ",");
152             }
153         }
154         Log.d(TAG, sb.toString());
155     }
156 
157     @Override
queryRoots(String[] projection)158     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
159         // It's possible that the folder does not exist on disk, so we will create the folder if
160         // that is the case. If user decides to delete the folder later, then it's OK to fail on
161         // subsequent queries.
162         getPublicDownloadsDirectory().mkdirs();
163 
164         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
165         final RowBuilder row = result.newRow();
166         row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
167         row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS
168                 | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH
169                 | Root.FLAG_SUPPORTS_IS_CHILD);
170         row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher_download);
171         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_downloads));
172         row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
173         row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
174         return result;
175     }
176 
177     @Override
findDocumentPath(@ullable String parentDocId, String docId)178     public Path findDocumentPath(@Nullable String parentDocId, String docId) throws FileNotFoundException {
179 
180         // parentDocId is null if the client is asking for the path to the root of a doc tree.
181         // Don't share root information with those who shouldn't know it.
182         final String rootId = (parentDocId == null) ? DOC_ID_ROOT : null;
183 
184         if (parentDocId == null) {
185             parentDocId = DOC_ID_ROOT;
186         }
187 
188         final File parent = getFileForDocId(parentDocId);
189 
190         final File doc = getFileForDocId(docId);
191 
192         return new Path(rootId, findDocumentPath(parent, doc));
193     }
194 
195     /**
196      * Calls on {@link FileSystemProvider#createDocument(String, String, String)}, and then creates
197      * a new database entry in {@link DownloadManager} if it is not a raw file and not a folder.
198      */
199     @Override
createDocument(String parentDocId, String mimeType, String displayName)200     public String createDocument(String parentDocId, String mimeType, String displayName)
201             throws FileNotFoundException {
202         // Delegate to real provider
203         final long token = Binder.clearCallingIdentity();
204         try {
205             String newDocumentId = super.createDocument(parentDocId, mimeType, displayName);
206             if (!Document.MIME_TYPE_DIR.equals(mimeType)
207                     && !RawDocumentsHelper.isRawDocId(parentDocId)
208                     && !isMediaStoreDownload(parentDocId)) {
209                 File newFile = getFileForDocId(newDocumentId);
210                 newDocumentId = Long.toString(mDm.addCompletedDownload(
211                         newFile.getName(), newFile.getName(), true, mimeType,
212                         newFile.getAbsolutePath(), 0L,
213                         false, true));
214             }
215             return newDocumentId;
216         } finally {
217             Binder.restoreCallingIdentity(token);
218         }
219     }
220 
221     @Override
deleteDocument(String docId)222     public void deleteDocument(String docId) throws FileNotFoundException {
223         // Delegate to real provider
224         final long token = Binder.clearCallingIdentity();
225         try {
226             if (RawDocumentsHelper.isRawDocId(docId) || isMediaStoreDownload(docId)) {
227                 super.deleteDocument(docId);
228                 return;
229             }
230 
231             if (mDm.remove(Long.parseLong(docId)) != 1) {
232                 throw new IllegalStateException("Failed to delete " + docId);
233             }
234         } finally {
235             Binder.restoreCallingIdentity(token);
236         }
237     }
238 
239     @Override
renameDocument(String docId, String displayName)240     public String renameDocument(String docId, String displayName)
241             throws FileNotFoundException {
242         final long token = Binder.clearCallingIdentity();
243 
244         try {
245             if (RawDocumentsHelper.isRawDocId(docId)
246                     || isMediaStoreDownloadDir(docId)) {
247                 return super.renameDocument(docId, displayName);
248             }
249 
250             displayName = FileUtils.buildValidFatFilename(displayName);
251             if (isMediaStoreDownload(docId)) {
252                 renameMediaStoreDownload(docId, displayName);
253             } else {
254                 final long id = Long.parseLong(docId);
255                 if (!mDm.rename(getContext(), id, displayName)) {
256                     throw new IllegalStateException(
257                             "Failed to rename to " + displayName + " in downloadsManager");
258                 }
259             }
260             return null;
261         } finally {
262             Binder.restoreCallingIdentity(token);
263         }
264     }
265 
266     @Override
queryDocument(String docId, String[] projection)267     public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
268         // Delegate to real provider
269         final long token = Binder.clearCallingIdentity();
270         Cursor cursor = null;
271         try {
272             if (RawDocumentsHelper.isRawDocId(docId)) {
273                 return super.queryDocument(docId, projection);
274             }
275 
276             final DownloadsCursor result = new DownloadsCursor(projection,
277                     getContext().getContentResolver());
278 
279             if (DOC_ID_ROOT.equals(docId)) {
280                 includeDefaultDocument(result);
281             } else if (isMediaStoreDownload(docId)) {
282                 cursor = getContext().getContentResolver().query(getMediaStoreUri(docId),
283                         null, null, null);
284                 copyNotificationUri(result, cursor);
285                 if (cursor.moveToFirst()) {
286                     includeDownloadFromMediaStore(result, cursor, null /* filePaths */);
287                 }
288             } else {
289                 cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
290                 copyNotificationUri(result, cursor);
291                 if (cursor.moveToFirst()) {
292                     // We don't know if this queryDocument() call is from Downloads (manage)
293                     // or Files. Safely assume it's Files.
294                     includeDownloadFromCursor(result, cursor, null /* filePaths */,
295                             null /* queryArgs */);
296                 }
297             }
298             result.start();
299             return result;
300         } finally {
301             IoUtils.closeQuietly(cursor);
302             Binder.restoreCallingIdentity(token);
303         }
304     }
305 
306     @Override
queryChildDocuments(String parentDocId, String[] projection, String sortOrder)307     public Cursor queryChildDocuments(String parentDocId, String[] projection, String sortOrder)
308             throws FileNotFoundException {
309         return queryChildDocuments(parentDocId, projection, sortOrder, false);
310     }
311 
312     @Override
queryChildDocumentsForManage( String parentDocId, String[] projection, String sortOrder)313     public Cursor queryChildDocumentsForManage(
314             String parentDocId, String[] projection, String sortOrder)
315             throws FileNotFoundException {
316         return queryChildDocuments(parentDocId, projection, sortOrder, true);
317     }
318 
queryChildDocuments(String parentDocId, String[] projection, String sortOrder, boolean manage)319     private Cursor queryChildDocuments(String parentDocId, String[] projection,
320             String sortOrder, boolean manage) throws FileNotFoundException {
321 
322         // Delegate to real provider
323         final long token = Binder.clearCallingIdentity();
324         Cursor cursor = null;
325         try {
326             if (RawDocumentsHelper.isRawDocId(parentDocId)) {
327                 return super.queryChildDocuments(parentDocId, projection, sortOrder);
328             }
329 
330             final DownloadsCursor result = new DownloadsCursor(projection,
331                     getContext().getContentResolver());
332             final ArrayList<Uri> notificationUris = new ArrayList<>();
333             if (isMediaStoreDownloadDir(parentDocId)) {
334                 includeDownloadsFromMediaStore(result, null /* queryArgs */,
335                         null /* filePaths */, notificationUris,
336                         getMediaStoreIdString(parentDocId), NO_LIMIT, manage);
337             } else {
338                 assert (DOC_ID_ROOT.equals(parentDocId));
339                 if (manage) {
340                     cursor = mDm.query(
341                             new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
342                 } else {
343                     cursor = mDm.query(
344                             new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
345                                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
346                 }
347                 final Set<String> filePaths = new HashSet<>();
348                 while (cursor.moveToNext()) {
349                     includeDownloadFromCursor(result, cursor, filePaths, null /* queryArgs */);
350                 }
351                 notificationUris.add(cursor.getNotificationUri());
352                 includeDownloadsFromMediaStore(result, null /* queryArgs */,
353                         filePaths, notificationUris,
354                         null /* parentId */, NO_LIMIT, manage);
355                 includeFilesFromSharedStorage(result, filePaths, null);
356             }
357             result.setNotificationUris(getContext().getContentResolver(), notificationUris);
358             result.start();
359             return result;
360         } finally {
361             IoUtils.closeQuietly(cursor);
362             Binder.restoreCallingIdentity(token);
363         }
364     }
365 
366     @Override
queryRecentDocuments(String rootId, String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal signal)367     public Cursor queryRecentDocuments(String rootId, String[] projection,
368             @Nullable Bundle queryArgs, @Nullable CancellationSignal signal)
369             throws FileNotFoundException {
370         final DownloadsCursor result =
371                 new DownloadsCursor(projection, getContext().getContentResolver());
372 
373         // Delegate to real provider
374         final long token = Binder.clearCallingIdentity();
375 
376         int limit = 12;
377         if (queryArgs != null) {
378             limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, -1);
379 
380             if (limit < 0) {
381                 // Use default value, and no QUERY_ARG* is honored.
382                 limit = 12;
383             } else {
384                 // We are honoring the QUERY_ARG_LIMIT.
385                 Bundle extras = new Bundle();
386                 result.setExtras(extras);
387                 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[]{
388                         ContentResolver.QUERY_ARG_LIMIT
389                 });
390             }
391         }
392 
393         Cursor cursor = null;
394         final ArrayList<Uri> notificationUris = new ArrayList<>();
395         try {
396             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
397                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
398             final Set<String> filePaths = new HashSet<>();
399             while (cursor.moveToNext() && result.getCount() < limit) {
400                 final String mimeType = cursor.getString(
401                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
402                 final String uri = cursor.getString(
403                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
404 
405                 // Skip images and videos that have been inserted into the MediaStore so we
406                 // don't duplicate them in the recent list. The audio root of
407                 // MediaDocumentsProvider doesn't support recent, we add it into recent list.
408                 if (mimeType == null || (MediaFile.isImageMimeType(mimeType)
409                         || MediaFile.isVideoMimeType(mimeType)) && !TextUtils.isEmpty(uri)) {
410                     continue;
411                 }
412                 includeDownloadFromCursor(result, cursor, filePaths,
413                         null /* queryArgs */);
414             }
415             notificationUris.add(cursor.getNotificationUri());
416 
417             // Skip media files that have been inserted into the MediaStore so we
418             // don't duplicate them in the recent list.
419             final Bundle args = new Bundle();
420             args.putBoolean(DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, true);
421 
422             includeDownloadsFromMediaStore(result, args, filePaths,
423                     notificationUris, null /* parentId */, (limit - result.getCount()),
424                     false /* includePending */);
425         } finally {
426             IoUtils.closeQuietly(cursor);
427             Binder.restoreCallingIdentity(token);
428         }
429 
430         result.setNotificationUris(getContext().getContentResolver(), notificationUris);
431         result.start();
432         return result;
433     }
434 
435     @Override
querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)436     public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
437             throws FileNotFoundException {
438 
439         final DownloadsCursor result =
440                 new DownloadsCursor(projection, getContext().getContentResolver());
441         final ArrayList<Uri> notificationUris = new ArrayList<>();
442 
443         // Delegate to real provider
444         final long token = Binder.clearCallingIdentity();
445         Cursor cursor = null;
446         try {
447             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
448                     .setFilterByString(DocumentsContract.getSearchDocumentsQuery(queryArgs)));
449             final Set<String> filePaths = new HashSet<>();
450             while (cursor.moveToNext()) {
451                 includeDownloadFromCursor(result, cursor, filePaths, queryArgs);
452             }
453             notificationUris.add(cursor.getNotificationUri());
454             includeDownloadsFromMediaStore(result, queryArgs, filePaths,
455                     notificationUris, null /* parentId */, NO_LIMIT, true /* includePending */);
456 
457             includeSearchFilesFromSharedStorage(result, projection, filePaths, queryArgs);
458         } finally {
459             IoUtils.closeQuietly(cursor);
460             Binder.restoreCallingIdentity(token);
461         }
462 
463         final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs);
464         if (handledQueryArgs.length > 0) {
465             final Bundle extras = new Bundle();
466             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs);
467             result.setExtras(extras);
468         }
469 
470         result.setNotificationUris(getContext().getContentResolver(), notificationUris);
471         result.start();
472         return result;
473     }
474 
includeSearchFilesFromSharedStorage(DownloadsCursor result, String[] projection, Set<String> filePaths, Bundle queryArgs)475     private void includeSearchFilesFromSharedStorage(DownloadsCursor result,
476             String[] projection, Set<String> filePaths,
477             Bundle queryArgs) throws FileNotFoundException {
478         final File downloadDir = getPublicDownloadsDirectory();
479         try (Cursor rawFilesCursor = super.querySearchDocuments(downloadDir,
480                 projection, filePaths, queryArgs)) {
481 
482             final boolean shouldExcludeMedia = queryArgs.getBoolean(
483                     DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
484             while (rawFilesCursor.moveToNext()) {
485                 final String mimeType = rawFilesCursor.getString(
486                         rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
487                 // When the value of shouldExcludeMedia is true, don't add media files into
488                 // the result to avoid duplicated files. MediaScanner will scan the files
489                 // into MediaStore. If the behavior is changed, we need to add the files back.
490                 if (!shouldExcludeMedia || !isMediaMimeType(mimeType)) {
491                     String docId = rawFilesCursor.getString(
492                             rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID));
493                     File rawFile = getFileForDocId(docId);
494                     includeFileFromSharedStorage(result, rawFile);
495                 }
496             }
497         }
498     }
499 
500     @Override
getDocumentType(String docId)501     public String getDocumentType(String docId) throws FileNotFoundException {
502         // Delegate to real provider
503         final long token = Binder.clearCallingIdentity();
504         try {
505             if (RawDocumentsHelper.isRawDocId(docId)) {
506                 return super.getDocumentType(docId);
507             }
508 
509             final ContentResolver resolver = getContext().getContentResolver();
510             final Uri contentUri;
511             if (isMediaStoreDownload(docId)) {
512                 contentUri = getMediaStoreUri(docId);
513             } else {
514                 final long id = Long.parseLong(docId);
515                 contentUri = mDm.getDownloadUri(id);
516             }
517             return resolver.getType(contentUri);
518         } finally {
519             Binder.restoreCallingIdentity(token);
520         }
521     }
522 
523     @Override
openDocument(String docId, String mode, CancellationSignal signal)524     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
525             throws FileNotFoundException {
526         // Delegate to real provider
527         final long token = Binder.clearCallingIdentity();
528         try {
529             if (RawDocumentsHelper.isRawDocId(docId)) {
530                 return super.openDocument(docId, mode, signal);
531             }
532 
533             final ContentResolver resolver = getContext().getContentResolver();
534             final Uri contentUri;
535             if (isMediaStoreDownload(docId)) {
536                 contentUri = getMediaStoreUri(docId);
537             } else {
538                 final long id = Long.parseLong(docId);
539                 contentUri = mDm.getDownloadUri(id);
540             }
541             return resolver.openFileDescriptor(contentUri, mode, signal);
542         } finally {
543             Binder.restoreCallingIdentity(token);
544         }
545     }
546 
547     @Override
getFileForDocId(String docId, boolean visible)548     protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
549         if (RawDocumentsHelper.isRawDocId(docId)) {
550             return new File(RawDocumentsHelper.getAbsoluteFilePath(docId));
551         }
552 
553         if (isMediaStoreDownload(docId)) {
554             return getFileForMediaStoreDownload(docId);
555         }
556 
557         if (DOC_ID_ROOT.equals(docId)) {
558             return getPublicDownloadsDirectory();
559         }
560 
561         final long token = Binder.clearCallingIdentity();
562         Cursor cursor = null;
563         String localFilePath = null;
564         try {
565             cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
566             if (cursor.moveToFirst()) {
567                 localFilePath = cursor.getString(
568                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
569             }
570         } finally {
571             IoUtils.closeQuietly(cursor);
572             Binder.restoreCallingIdentity(token);
573         }
574 
575         if (localFilePath == null) {
576             throw new IllegalStateException("File has no filepath. Could not be found.");
577         }
578         return new File(localFilePath);
579     }
580 
581     @Override
getDocIdForFile(File file)582     protected String getDocIdForFile(File file) throws FileNotFoundException {
583         return RawDocumentsHelper.getDocIdForFile(file);
584     }
585 
586     @Override
buildNotificationUri(String docId)587     protected Uri buildNotificationUri(String docId) {
588         return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
589     }
590 
isMediaMimeType(String mimeType)591     private static boolean isMediaMimeType(String mimeType) {
592         return MediaFile.isImageMimeType(mimeType) || MediaFile.isVideoMimeType(mimeType)
593                 || MediaFile.isAudioMimeType(mimeType);
594     }
595 
includeDefaultDocument(MatrixCursor result)596     private void includeDefaultDocument(MatrixCursor result) {
597         final RowBuilder row = result.newRow();
598         row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
599         // We have the same display name as our root :)
600         row.add(Document.COLUMN_DISPLAY_NAME,
601                 getContext().getString(R.string.root_downloads));
602         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
603         row.add(Document.COLUMN_FLAGS,
604                 Document.FLAG_DIR_PREFERS_LAST_MODIFIED | Document.FLAG_DIR_SUPPORTS_CREATE);
605     }
606 
607     /**
608      * Adds the entry from the cursor to the result only if the entry is valid. That is,
609      * if the file exists in the file system.
610      */
includeDownloadFromCursor(MatrixCursor result, Cursor cursor, Set<String> filePaths, Bundle queryArgs)611     private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor,
612             Set<String> filePaths, Bundle queryArgs) {
613         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
614         final String docId = String.valueOf(id);
615 
616         final String displayName = cursor.getString(
617                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
618         String summary = cursor.getString(
619                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
620         String mimeType = cursor.getString(
621                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
622         if (mimeType == null) {
623             // Provide fake MIME type so it's openable
624             mimeType = "vnd.android.document/file";
625         }
626 
627         if (queryArgs != null) {
628             final boolean shouldExcludeMedia = queryArgs.getBoolean(
629                     DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
630             if (shouldExcludeMedia) {
631                 final String uri = cursor.getString(
632                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
633 
634                 // Skip media files that have been inserted into the MediaStore so we
635                 // don't duplicate them in the search list.
636                 if (isMediaMimeType(mimeType) && !TextUtils.isEmpty(uri)) {
637                     return;
638                 }
639             }
640         }
641 
642         // size could be -1 which indicates that download hasn't started.
643         final long size = cursor.getLong(
644                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
645 
646         String localFilePath = cursor.getString(
647                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
648 
649         int extraFlags = Document.FLAG_PARTIAL;
650         final int status = cursor.getInt(
651                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
652         switch (status) {
653             case DownloadManager.STATUS_SUCCESSFUL:
654                 // Verify that the document still exists in external storage. This is necessary
655                 // because files can be deleted from the file system without their entry being
656                 // removed from DownloadsManager.
657                 if (localFilePath == null || !new File(localFilePath).exists()) {
658                     return;
659                 }
660                 extraFlags = Document.FLAG_SUPPORTS_RENAME;  // only successful is non-partial
661                 break;
662             case DownloadManager.STATUS_PAUSED:
663                 summary = getContext().getString(R.string.download_queued);
664                 break;
665             case DownloadManager.STATUS_PENDING:
666                 summary = getContext().getString(R.string.download_queued);
667                 break;
668             case DownloadManager.STATUS_RUNNING:
669                 final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
670                         DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
671                 if (size > 0) {
672                     String percent =
673                             NumberFormat.getPercentInstance().format((double) progress / size);
674                     summary = getContext().getString(R.string.download_running_percent, percent);
675                 } else {
676                     summary = getContext().getString(R.string.download_running);
677                 }
678                 break;
679             case DownloadManager.STATUS_FAILED:
680             default:
681                 summary = getContext().getString(R.string.download_error);
682                 break;
683         }
684 
685         final long lastModified = cursor.getLong(
686                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
687 
688         if (!DocumentsContract.matchSearchQueryArguments(queryArgs, displayName, mimeType,
689                 lastModified, size)) {
690             return;
691         }
692 
693         includeDownload(result, docId, displayName, summary, size, mimeType,
694                 lastModified, extraFlags, status == DownloadManager.STATUS_RUNNING);
695         if (filePaths != null && localFilePath != null) {
696             filePaths.add(localFilePath);
697         }
698     }
699 
includeDownload(MatrixCursor result, String docId, String displayName, String summary, long size, String mimeType, long lastModifiedMs, int extraFlags, boolean isPending)700     private void includeDownload(MatrixCursor result,
701             String docId, String displayName, String summary, long size,
702             String mimeType, long lastModifiedMs, int extraFlags, boolean isPending) {
703 
704         int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | extraFlags;
705         if (mimeType.startsWith("image/")) {
706             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
707         }
708 
709         if (typeSupportsMetadata(mimeType)) {
710             flags |= Document.FLAG_SUPPORTS_METADATA;
711         }
712 
713         final RowBuilder row = result.newRow();
714         row.add(Document.COLUMN_DOCUMENT_ID, docId);
715         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
716         row.add(Document.COLUMN_SUMMARY, summary);
717         row.add(Document.COLUMN_SIZE, size == -1 ? null : size);
718         row.add(Document.COLUMN_MIME_TYPE, mimeType);
719         row.add(Document.COLUMN_FLAGS, flags);
720         // Incomplete downloads get a null timestamp.  This prevents thrashy UI when a bunch of
721         // active downloads get sorted by mod time.
722         if (!isPending) {
723             row.add(Document.COLUMN_LAST_MODIFIED, lastModifiedMs);
724         }
725     }
726 
727     /**
728      * Takes all the top-level files from the Downloads directory and adds them to the result.
729      *
730      * @param result cursor containing all documents to be returned by queryChildDocuments or
731      *            queryChildDocumentsForManage.
732      * @param downloadedFilePaths The absolute file paths of all the files in the result Cursor.
733      * @param searchString query used to filter out unwanted results.
734      */
includeFilesFromSharedStorage(DownloadsCursor result, Set<String> downloadedFilePaths, @Nullable String searchString)735     private void includeFilesFromSharedStorage(DownloadsCursor result,
736             Set<String> downloadedFilePaths, @Nullable String searchString)
737             throws FileNotFoundException {
738         final File downloadsDir = getPublicDownloadsDirectory();
739         // Add every file from the Downloads directory to the result cursor. Ignore files that
740         // were in the supplied downloaded file paths.
741         for (File file : FileUtils.listFilesOrEmpty(downloadsDir)) {
742             boolean inResultsAlready = downloadedFilePaths.contains(file.getAbsolutePath());
743             boolean containsQuery = searchString == null || file.getName().contains(
744                     searchString);
745             if (!inResultsAlready && containsQuery) {
746                 includeFileFromSharedStorage(result, file);
747             }
748         }
749     }
750 
751     /**
752      * Adds a file to the result cursor. It uses a combination of {@code #RAW_PREFIX} and its
753      * absolute file path for its id. Directories are not to be included.
754      *
755      * @param result cursor containing all documents to be returned by queryChildDocuments or
756      *            queryChildDocumentsForManage.
757      * @param file file to be included in the result cursor.
758      */
includeFileFromSharedStorage(MatrixCursor result, File file)759     private void includeFileFromSharedStorage(MatrixCursor result, File file)
760             throws FileNotFoundException {
761         includeFile(result, null, file);
762     }
763 
getPublicDownloadsDirectory()764     private static File getPublicDownloadsDirectory() {
765         return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
766     }
767 
renameMediaStoreDownload(String docId, String displayName)768     private void renameMediaStoreDownload(String docId, String displayName) {
769         final File before = getFileForMediaStoreDownload(docId);
770         final File after = new File(before.getParentFile(), displayName);
771 
772         if (after.exists()) {
773             throw new IllegalStateException("Already exists " + after);
774         }
775         if (!before.renameTo(after)) {
776             throw new IllegalStateException("Failed to rename from " + before + " to " + after);
777         }
778 
779         final long token = Binder.clearCallingIdentity();
780         try {
781             final Uri mediaStoreUri = getMediaStoreUri(docId);
782             final ContentValues values = new ContentValues();
783             values.put(DownloadColumns.DATA, after.getAbsolutePath());
784             values.put(DownloadColumns.DISPLAY_NAME, displayName);
785             final int count = getContext().getContentResolver().update(mediaStoreUri, values,
786                     null, null);
787             if (count != 1) {
788                 throw new IllegalStateException("Failed to update " + mediaStoreUri
789                         + ", values=" + values);
790             }
791         } finally {
792             Binder.restoreCallingIdentity(token);
793         }
794     }
795 
getFileForMediaStoreDownload(String docId)796     private File getFileForMediaStoreDownload(String docId) {
797         final Uri mediaStoreUri = getMediaStoreUri(docId);
798         final long token = Binder.clearCallingIdentity();
799         try (Cursor cursor = queryForSingleItem(mediaStoreUri,
800                 new String[] { DownloadColumns.DATA }, null, null, null)) {
801             final String filePath = cursor.getString(0);
802             if (filePath == null) {
803                 throw new IllegalStateException("Missing _data for " + mediaStoreUri);
804             }
805             return new File(filePath);
806         } catch (FileNotFoundException e) {
807             throw new IllegalStateException(e);
808         } finally {
809             Binder.restoreCallingIdentity(token);
810         }
811     }
812 
getRelativePathAndDisplayNameForDownload(long id)813     private Pair<String, String> getRelativePathAndDisplayNameForDownload(long id) {
814         final Uri mediaStoreUri = ContentUris.withAppendedId(
815                 MediaStore.Downloads.EXTERNAL_CONTENT_URI, id);
816         final long token = Binder.clearCallingIdentity();
817         try (Cursor cursor = queryForSingleItem(mediaStoreUri,
818                 new String[] { DownloadColumns.RELATIVE_PATH, DownloadColumns.DISPLAY_NAME },
819                 null, null, null)) {
820             final String relativePath = cursor.getString(0);
821             final String displayName = cursor.getString(1);
822             if (relativePath == null || displayName == null) {
823                 throw new IllegalStateException(
824                         "relative_path and _display_name should not be null for " + mediaStoreUri);
825             }
826             return Pair.create(relativePath, displayName);
827         } catch (FileNotFoundException e) {
828             throw new IllegalStateException(e);
829         } finally {
830             Binder.restoreCallingIdentity(token);
831         }
832     }
833 
834     /**
835      * Copied from MediaProvider.java
836      *
837      * Query the given {@link Uri}, expecting only a single item to be found.
838      *
839      * @throws FileNotFoundException if no items were found, or multiple items
840      *             were found, or there was trouble reading the data.
841      */
queryForSingleItem(Uri uri, String[] projection, String selection, String[] selectionArgs, CancellationSignal signal)842     private Cursor queryForSingleItem(Uri uri, String[] projection,
843             String selection, String[] selectionArgs, CancellationSignal signal)
844             throws FileNotFoundException {
845         final Cursor c = getContext().getContentResolver().query(uri, projection,
846                 ContentResolver.createSqlQueryBundle(selection, selectionArgs, null), signal);
847         if (c == null) {
848             throw new FileNotFoundException("Missing cursor for " + uri);
849         } else if (c.getCount() < 1) {
850             IoUtils.closeQuietly(c);
851             throw new FileNotFoundException("No item at " + uri);
852         } else if (c.getCount() > 1) {
853             IoUtils.closeQuietly(c);
854             throw new FileNotFoundException("Multiple items at " + uri);
855         }
856 
857         if (c.moveToFirst()) {
858             return c;
859         } else {
860             IoUtils.closeQuietly(c);
861             throw new FileNotFoundException("Failed to read row from " + uri);
862         }
863     }
864 
includeDownloadsFromMediaStore(@onNull MatrixCursor result, @Nullable Bundle queryArgs, @Nullable Set<String> filePaths, @NonNull ArrayList<Uri> notificationUris, @Nullable String parentId, int limit, boolean includePending)865     private void includeDownloadsFromMediaStore(@NonNull MatrixCursor result,
866             @Nullable Bundle queryArgs,
867             @Nullable Set<String> filePaths, @NonNull ArrayList<Uri> notificationUris,
868             @Nullable String parentId, int limit, boolean includePending) {
869         if (limit == 0) {
870             return;
871         }
872 
873         final long token = Binder.clearCallingIdentity();
874         final Pair<String, String[]> selectionPair
875                 = buildSearchSelection(queryArgs, filePaths, parentId);
876         final Uri.Builder queryUriBuilder = MediaStore.Downloads.EXTERNAL_CONTENT_URI.buildUpon();
877         if (limit != NO_LIMIT) {
878             queryUriBuilder.appendQueryParameter(MediaStore.PARAM_LIMIT, String.valueOf(limit));
879         }
880         if (includePending) {
881             MediaStore.setIncludePending(queryUriBuilder);
882         }
883         try (Cursor cursor = getContext().getContentResolver().query(
884                 queryUriBuilder.build(), null,
885                 selectionPair.first, selectionPair.second, null)) {
886             while (cursor.moveToNext()) {
887                 includeDownloadFromMediaStore(result, cursor, filePaths);
888             }
889             notificationUris.add(MediaStore.Files.EXTERNAL_CONTENT_URI);
890             notificationUris.add(MediaStore.Downloads.EXTERNAL_CONTENT_URI);
891         } finally {
892             Binder.restoreCallingIdentity(token);
893         }
894     }
895 
includeDownloadFromMediaStore(@onNull MatrixCursor result, @NonNull Cursor mediaCursor, @Nullable Set<String> filePaths)896     private void includeDownloadFromMediaStore(@NonNull MatrixCursor result,
897             @NonNull Cursor mediaCursor, @Nullable Set<String> filePaths) {
898         final String mimeType = getMimeType(mediaCursor);
899         final boolean isDir = Document.MIME_TYPE_DIR.equals(mimeType);
900         final String docId = getDocIdForMediaStoreDownload(
901                 mediaCursor.getLong(mediaCursor.getColumnIndex(DownloadColumns._ID)), isDir);
902         final String displayName = mediaCursor.getString(
903                 mediaCursor.getColumnIndex(DownloadColumns.DISPLAY_NAME));
904         final long size = mediaCursor.getLong(
905                 mediaCursor.getColumnIndex(DownloadColumns.SIZE));
906         final long lastModifiedMs = mediaCursor.getLong(
907                 mediaCursor.getColumnIndex(DownloadColumns.DATE_MODIFIED)) * 1000;
908         final boolean isPending = mediaCursor.getInt(
909                 mediaCursor.getColumnIndex(DownloadColumns.IS_PENDING)) == 1;
910 
911         int extraFlags = isPending ? Document.FLAG_PARTIAL : 0;
912         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
913             extraFlags |= Document.FLAG_DIR_SUPPORTS_CREATE;
914         }
915         if (!isPending) {
916             extraFlags |= Document.FLAG_SUPPORTS_RENAME;
917         }
918 
919         includeDownload(result, docId, displayName, null /* description */, size, mimeType,
920                 lastModifiedMs, extraFlags, isPending);
921         if (filePaths != null) {
922             filePaths.add(mediaCursor.getString(
923                     mediaCursor.getColumnIndex(DownloadColumns.DATA)));
924         }
925     }
926 
getMimeType(@onNull Cursor mediaCursor)927     private String getMimeType(@NonNull Cursor mediaCursor) {
928         final String mimeType = mediaCursor.getString(
929                 mediaCursor.getColumnIndex(DownloadColumns.MIME_TYPE));
930         if (mimeType == null) {
931             return Document.MIME_TYPE_DIR;
932         }
933         return mimeType;
934     }
935 
936     // Copied from MediaDocumentsProvider with some tweaks
buildSearchSelection(@ullable Bundle queryArgs, @Nullable Set<String> filePaths, @Nullable String parentId)937     private Pair<String, String[]> buildSearchSelection(@Nullable Bundle queryArgs,
938             @Nullable Set<String> filePaths, @Nullable String parentId) {
939         final StringBuilder selection = new StringBuilder();
940         final ArrayList<String> selectionArgs = new ArrayList<>();
941 
942         if (parentId == null && filePaths != null && filePaths.size() > 0) {
943             if (selection.length() > 0) {
944                 selection.append(" AND ");
945             }
946             selection.append(DownloadColumns.DATA + " NOT IN (");
947             selection.append(TextUtils.join(",", Collections.nCopies(filePaths.size(), "?")));
948             selection.append(")");
949             selectionArgs.addAll(filePaths);
950         }
951 
952         if (parentId != null) {
953             if (selection.length() > 0) {
954                 selection.append(" AND ");
955             }
956             selection.append(DownloadColumns.RELATIVE_PATH + "=?");
957             final Pair<String, String> data = getRelativePathAndDisplayNameForDownload(
958                     Long.parseLong(parentId));
959             selectionArgs.add(data.first + data.second + "/");
960         } else {
961             if (selection.length() > 0) {
962                 selection.append(" AND ");
963             }
964             selection.append(DownloadColumns.RELATIVE_PATH + "=?");
965             selectionArgs.add(Environment.DIRECTORY_DOWNLOADS + "/");
966         }
967 
968         if (queryArgs != null) {
969             final boolean shouldExcludeMedia = queryArgs.getBoolean(
970                     DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
971             if (shouldExcludeMedia) {
972                 if (selection.length() > 0) {
973                     selection.append(" AND ");
974                 }
975                 selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"image/%\"");
976                 selection.append(" AND ");
977                 selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"audio/%\"");
978                 selection.append(" AND ");
979                 selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"video/%\"");
980             }
981 
982             final String displayName = queryArgs.getString(
983                     DocumentsContract.QUERY_ARG_DISPLAY_NAME);
984             if (!TextUtils.isEmpty(displayName)) {
985                 if (selection.length() > 0) {
986                     selection.append(" AND ");
987                 }
988                 selection.append(DownloadColumns.DISPLAY_NAME + " LIKE ?");
989                 selectionArgs.add("%" + displayName + "%");
990             }
991 
992             final long lastModifiedAfter = queryArgs.getLong(
993                     DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, -1 /* defaultValue */);
994             if (lastModifiedAfter != -1) {
995                 if (selection.length() > 0) {
996                     selection.append(" AND ");
997                 }
998                 selection.append(DownloadColumns.DATE_MODIFIED
999                         + " > " + lastModifiedAfter / 1000);
1000             }
1001 
1002             final long fileSizeOver = queryArgs.getLong(
1003                     DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, -1 /* defaultValue */);
1004             if (fileSizeOver != -1) {
1005                 if (selection.length() > 0) {
1006                     selection.append(" AND ");
1007                 }
1008                 selection.append(DownloadColumns.SIZE + " > " + fileSizeOver);
1009             }
1010 
1011             final String[] mimeTypes = queryArgs.getStringArray(
1012                     DocumentsContract.QUERY_ARG_MIME_TYPES);
1013             if (mimeTypes != null && mimeTypes.length > 0) {
1014                 if (selection.length() > 0) {
1015                     selection.append(" AND ");
1016                 }
1017                 selection.append(DownloadColumns.MIME_TYPE + " IN (");
1018                 for (int i = 0; i < mimeTypes.length; ++i) {
1019                     selection.append("?").append((i == mimeTypes.length - 1) ? ")" : ",");
1020                     selectionArgs.add(mimeTypes[i]);
1021                 }
1022             }
1023         }
1024 
1025         return new Pair<>(selection.toString(), selectionArgs.toArray(new String[0]));
1026     }
1027 
1028     /**
1029      * A MatrixCursor that spins up a file observer when the first instance is
1030      * started ({@link #start()}, and stops the file observer when the last instance
1031      * closed ({@link #close()}. When file changes are observed, a content change
1032      * notification is sent on the Downloads content URI.
1033      *
1034      * <p>This is necessary as other processes, like ExternalStorageProvider,
1035      * can access and modify files directly (without sending operations
1036      * through DownloadStorageProvider).
1037      *
1038      * <p>Without this, contents accessible by one a Downloads cursor instance
1039      * (like the Downloads root in Files app) can become state.
1040      */
1041     private static final class DownloadsCursor extends MatrixCursor {
1042 
1043         private static final Object mLock = new Object();
1044         @GuardedBy("mLock")
1045         private static int mOpenCursorCount = 0;
1046         @GuardedBy("mLock")
1047         private static @Nullable ContentChangedRelay mFileWatcher;
1048 
1049         private final ContentResolver mResolver;
1050 
DownloadsCursor(String[] projection, ContentResolver resolver)1051         DownloadsCursor(String[] projection, ContentResolver resolver) {
1052             super(resolveDocumentProjection(projection));
1053             mResolver = resolver;
1054         }
1055 
start()1056         void start() {
1057             synchronized (mLock) {
1058                 if (mOpenCursorCount++ == 0) {
1059                     mFileWatcher = new ContentChangedRelay(mResolver,
1060                             Arrays.asList(getPublicDownloadsDirectory()));
1061                     mFileWatcher.startWatching();
1062                 }
1063             }
1064         }
1065 
1066         @Override
close()1067         public void close() {
1068             super.close();
1069             synchronized (mLock) {
1070                 if (--mOpenCursorCount == 0) {
1071                     mFileWatcher.stopWatching();
1072                     mFileWatcher = null;
1073                 }
1074             }
1075         }
1076     }
1077 
1078     /**
1079      * A file observer that notifies on the Downloads content URI(s) when
1080      * files change on disk.
1081      */
1082     private static class ContentChangedRelay extends FileObserver {
1083         private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
1084                 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
1085 
1086         private File[] mDownloadDirs;
1087         private final ContentResolver mResolver;
1088 
ContentChangedRelay(ContentResolver resolver, List<File> downloadDirs)1089         public ContentChangedRelay(ContentResolver resolver, List<File> downloadDirs) {
1090             super(downloadDirs, NOTIFY_EVENTS);
1091             mDownloadDirs = downloadDirs.toArray(new File[0]);
1092             mResolver = resolver;
1093         }
1094 
1095         @Override
startWatching()1096         public void startWatching() {
1097             super.startWatching();
1098             if (DEBUG) Log.d(TAG, "Started watching for file changes in: "
1099                     + Arrays.toString(mDownloadDirs));
1100         }
1101 
1102         @Override
stopWatching()1103         public void stopWatching() {
1104             super.stopWatching();
1105             if (DEBUG) Log.d(TAG, "Stopped watching for file changes in: "
1106                     + Arrays.toString(mDownloadDirs));
1107         }
1108 
1109         @Override
onEvent(int event, String path)1110         public void onEvent(int event, String path) {
1111             if ((event & NOTIFY_EVENTS) != 0) {
1112                 if (DEBUG) Log.v(TAG, "Change detected at path: " + path);
1113                 mResolver.notifyChange(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, false);
1114                 mResolver.notifyChange(Downloads.Impl.CONTENT_URI, null, false);
1115             }
1116         }
1117     }
1118 }
1119