1 /*
2  * Copyright (C) 2019 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 package com.android.providers.downloads;
17 
18 import static android.provider.BaseColumns._ID;
19 import static android.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
20 import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
21 import static android.provider.Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI;
22 import static android.provider.Downloads.Impl.COLUMN_MEDIASTORE_URI;
23 import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
24 import static android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
25 import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
26 import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
27 import static android.provider.Downloads.Impl.MEDIA_SCANNED;
28 import static android.provider.Downloads.Impl._DATA;
29 
30 import static com.android.providers.downloads.Constants.MEDIA_SCAN_TRIGGER_JOB_ID;
31 
32 import android.app.job.JobInfo;
33 import android.app.job.JobParameters;
34 import android.app.job.JobScheduler;
35 import android.app.job.JobService;
36 import android.content.ComponentName;
37 import android.content.ContentProviderClient;
38 import android.content.ContentValues;
39 import android.content.Context;
40 import android.database.Cursor;
41 import android.net.Uri;
42 import android.os.Environment;
43 import android.provider.Downloads;
44 import android.provider.MediaStore;
45 import android.util.Log;
46 
47 import java.io.File;
48 
49 /**
50  * Job to update MediaProvider with all the downloads and force mediascan on them.
51  */
52 public class MediaScanTriggerJob extends JobService {
53     private volatile boolean mJobStopped;
54 
55     @Override
onStartJob(JobParameters parameters)56     public boolean onStartJob(JobParameters parameters) {
57         if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
58             // External storage is not available yet, so defer this job to a later time.
59             jobFinished(parameters, true /* reschedule */);
60             return false;
61         }
62         Helpers.getAsyncHandler().post(() -> {
63             final String selection = _DATA + " IS NOT NULL"
64                     + " AND (" + COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + "=1"
65                     + " OR " + COLUMN_MEDIA_SCANNED + "=" + MEDIA_SCANNED + ")"
66                     + " AND (" + COLUMN_DESTINATION + "=" + DESTINATION_EXTERNAL
67                     + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_FILE_URI
68                     + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
69                     + ")";
70             try (ContentProviderClient cpc
71                     = getContentResolver().acquireContentProviderClient(Downloads.Impl.AUTHORITY);
72                  ContentProviderClient mediaProviderClient
73                     = getContentResolver().acquireContentProviderClient(MediaStore.AUTHORITY)) {
74                 final DownloadProvider downloadProvider
75                         = ((DownloadProvider) cpc.getLocalContentProvider());
76                 try (Cursor cursor = downloadProvider.query(ALL_DOWNLOADS_CONTENT_URI,
77                         null, selection, null, null)) {
78 
79                     final DownloadInfo.Reader reader
80                             = new DownloadInfo.Reader(getContentResolver(), cursor);
81                     final DownloadInfo info = new DownloadInfo(MediaScanTriggerJob.this);
82                     while (cursor.moveToNext()) {
83                         if (mJobStopped) {
84                             return;
85                         }
86                         reader.updateFromDatabase(info);
87                         // This indicates that this entry has been handled already (perhaps when
88                         // this job ran earlier and got preempted), so skip.
89                         if (info.mMediaStoreUri != null) {
90                             continue;
91                         }
92                         final ContentValues mediaValues;
93                         try {
94                             mediaValues = downloadProvider.convertToMediaProviderValues(info);
95                         } catch (IllegalArgumentException e) {
96                             Log.e(Constants.TAG,
97                                     "Error getting media content values from " + info, e);
98                             continue;
99                         }
100                         // Overriding size column value to 0 for forcing the mediascan
101                         // later (to address http://b/138419471).
102                         mediaValues.put(MediaStore.Files.FileColumns.SIZE, 0);
103                         downloadProvider.updateMediaProvider(mediaProviderClient, mediaValues);
104 
105                         final Uri mediaStoreUri = Helpers.triggerMediaScan(mediaProviderClient,
106                                 new File(info.mFileName));
107                         if (mediaStoreUri != null) {
108                             final ContentValues downloadValues = new ContentValues();
109                             downloadValues.put(COLUMN_MEDIASTORE_URI, mediaStoreUri.toString());
110                             downloadProvider.update(ALL_DOWNLOADS_CONTENT_URI,
111                                     downloadValues, _ID + "=" + info.mId, null);
112                         }
113                     }
114                 }
115             }
116             jobFinished(parameters, false /* reschedule */);
117         });
118         return true;
119     }
120 
121     @Override
onStopJob(JobParameters parameters)122     public boolean onStopJob(JobParameters parameters) {
123         mJobStopped = true;
124         return true;
125     }
126 
schedule(Context context)127     public static void schedule(Context context) {
128         final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
129         final JobInfo job = new JobInfo.Builder(MEDIA_SCAN_TRIGGER_JOB_ID,
130                 new ComponentName(context, MediaScanTriggerJob.class))
131                         .setRequiresCharging(true)
132                         .setRequiresDeviceIdle(true)
133                         .build();
134         scheduler.schedule(job);
135     }
136 }
137