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