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 17 package com.android.providers.media; 18 19 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND; 20 import static com.android.providers.media.scan.MediaScanner.REASON_MOUNTED; 21 import static com.android.providers.media.util.Logging.TAG; 22 23 import android.content.ContentProviderClient; 24 import android.content.ContentResolver; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.media.RingtoneManager; 29 import android.net.Uri; 30 import android.os.Trace; 31 import android.os.UserHandle; 32 import android.os.storage.StorageVolume; 33 import android.provider.MediaStore; 34 import android.util.Log; 35 36 import androidx.core.app.JobIntentService; 37 38 import com.android.providers.media.util.FileUtils; 39 40 import java.io.File; 41 import java.io.IOException; 42 43 public class MediaService extends JobIntentService { 44 private static final int JOB_ID = -300; 45 46 private static final String ACTION_SCAN_VOLUME 47 = "com.android.providers.media.action.SCAN_VOLUME"; 48 49 private static final String EXTRA_MEDIAVOLUME = "MediaVolume"; 50 51 private static final String EXTRA_SCAN_REASON = "scan_reason"; 52 53 queueVolumeScan(Context context, MediaVolume volume, int reason)54 public static void queueVolumeScan(Context context, MediaVolume volume, int reason) { 55 Intent intent = new Intent(ACTION_SCAN_VOLUME); 56 intent.putExtra(EXTRA_MEDIAVOLUME, volume) ; 57 intent.putExtra(EXTRA_SCAN_REASON, reason); 58 enqueueWork(context, intent); 59 } 60 enqueueWork(Context context, Intent work)61 public static void enqueueWork(Context context, Intent work) { 62 enqueueWork(context, MediaService.class, JOB_ID, work); 63 } 64 65 @Override onHandleWork(Intent intent)66 protected void onHandleWork(Intent intent) { 67 Trace.beginSection("MediaService.handle[" + intent.getAction() + ']'); 68 if (Log.isLoggable(TAG, Log.INFO)) { 69 Log.i(TAG, "Begin " + intent); 70 } 71 try { 72 switch (intent.getAction()) { 73 case Intent.ACTION_LOCALE_CHANGED: { 74 onLocaleChanged(); 75 break; 76 } 77 case Intent.ACTION_PACKAGE_FULLY_REMOVED: 78 case Intent.ACTION_PACKAGE_DATA_CLEARED: { 79 final String packageName = intent.getData().getSchemeSpecificPart(); 80 final int uid = intent.getIntExtra(Intent.EXTRA_UID, 0); 81 onPackageOrphaned(packageName, uid); 82 break; 83 } 84 case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE: { 85 onScanFile(this, intent.getData()); 86 break; 87 } 88 case Intent.ACTION_MEDIA_MOUNTED: { 89 onMediaMountedBroadcast(this, intent); 90 break; 91 } 92 case ACTION_SCAN_VOLUME: { 93 final MediaVolume volume = intent.getParcelableExtra(EXTRA_MEDIAVOLUME); 94 int reason = intent.getIntExtra(EXTRA_SCAN_REASON, REASON_DEMAND); 95 onScanVolume(this, volume, reason); 96 break; 97 } 98 default: { 99 Log.w(TAG, "Unknown intent " + intent); 100 break; 101 } 102 } 103 } catch (Exception e) { 104 Log.w(TAG, "Failed operation " + intent, e); 105 } finally { 106 if (Log.isLoggable(TAG, Log.INFO)) { 107 Log.i(TAG, "End " + intent); 108 } 109 Trace.endSection(); 110 } 111 } 112 onLocaleChanged()113 private void onLocaleChanged() { 114 try (ContentProviderClient cpc = getContentResolver() 115 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 116 ((MediaProvider) cpc.getLocalContentProvider()).onLocaleChanged(); 117 } 118 } 119 onPackageOrphaned(String packageName, int uid)120 private void onPackageOrphaned(String packageName, int uid) { 121 try (ContentProviderClient cpc = getContentResolver() 122 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 123 ((MediaProvider) cpc.getLocalContentProvider()).onPackageOrphaned(packageName, uid); 124 } 125 } 126 onMediaMountedBroadcast(Context context, Intent intent)127 private static void onMediaMountedBroadcast(Context context, Intent intent) 128 throws IOException { 129 final StorageVolume volume = intent.getParcelableExtra(StorageVolume.EXTRA_STORAGE_VOLUME); 130 if (volume != null) { 131 MediaVolume mediaVolume = MediaVolume.fromStorageVolume(volume); 132 try (ContentProviderClient cpc = context.getContentResolver() 133 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 134 if (!((MediaProvider)cpc.getLocalContentProvider()).isVolumeAttached(mediaVolume)) { 135 // This can happen on some legacy app clone implementations, where the 136 // framework is modified to send MEDIA_MOUNTED broadcasts for clone volumes 137 // to u0 MediaProvider; these volumes are not reported through the usual 138 // volume attach events, so we need to scan them here if they weren't 139 // attached previously 140 onScanVolume(context, mediaVolume, REASON_MOUNTED); 141 } else { 142 Log.i(TAG, "Volume " + mediaVolume + " already attached"); 143 } 144 } 145 } else { 146 Log.e(TAG, "Couldn't retrieve StorageVolume from intent"); 147 } 148 } 149 onScanVolume(Context context, MediaVolume volume, int reason)150 public static void onScanVolume(Context context, MediaVolume volume, int reason) 151 throws IOException { 152 final String volumeName = volume.getName(); 153 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && volume.getPath() == null) { 154 /* This is a very unexpected state and can only ever happen with app-cloned users. 155 In general, MediaVolumes should always be mounted and have a path, however, if the 156 user failed to unlock properly, MediaProvider still gets the volume from the 157 StorageManagerService because MediaProvider is special cased there. See 158 StorageManagerService#getVolumeList. Reference bug: b/207723670. */ 159 Log.w(TAG, String.format("Skipping volume scan for %s when volume path is null.", 160 volumeName)); 161 return; 162 } 163 UserHandle owner = volume.getUser(); 164 if (owner == null) { 165 // Can happen for the internal volume 166 owner = context.getUser(); 167 } 168 // If we're about to scan any external storage, scan internal first 169 // to ensure that we have ringtones ready to roll before a possibly very 170 // long external storage scan 171 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 172 onScanVolume(context, MediaVolume.fromInternal(), reason); 173 RingtoneManager.ensureDefaultRingtones(context); 174 } 175 176 // Resolve the Uri that we should use for all broadcast intents related 177 // to this volume; we do this once to ensure we can deliver all events 178 // in the situation where a volume is ejected mid-scan 179 final Uri broadcastUri; 180 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 181 broadcastUri = Uri.fromFile(volume.getPath()); 182 } else { 183 broadcastUri = null; 184 } 185 186 try (ContentProviderClient cpc = context.getContentResolver() 187 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 188 final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider()); 189 provider.attachVolume(volume, /* validate */ true, /* volumeState */ null); 190 191 final ContentResolver resolver = ContentResolver.wrap(cpc.getLocalContentProvider()); 192 193 ContentValues values = new ContentValues(); 194 values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName); 195 Uri scanUri = resolver.insert(MediaStore.getMediaScannerUri(), values); 196 197 if (broadcastUri != null) { 198 context.sendBroadcastAsUser( 199 new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, broadcastUri), owner); 200 } 201 202 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 203 for (File dir : FileUtils.getVolumeScanPaths(context, volumeName)) { 204 provider.scanDirectory(dir, reason); 205 } 206 } else { 207 provider.scanDirectory(volume.getPath(), reason); 208 } 209 210 resolver.delete(scanUri, null, null); 211 212 } finally { 213 if (broadcastUri != null) { 214 context.sendBroadcastAsUser( 215 new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, broadcastUri), owner); 216 } 217 } 218 } 219 onScanFile(Context context, Uri uri)220 private static Uri onScanFile(Context context, Uri uri) throws IOException { 221 final File file = new File(uri.getPath()).getCanonicalFile(); 222 try (ContentProviderClient cpc = context.getContentResolver() 223 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 224 final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider()); 225 return provider.scanFile(file, REASON_DEMAND); 226 } 227 } 228 229 @Override onStopCurrentWork()230 public boolean onStopCurrentWork() { 231 // Scans are not stopped even if the job is stopped. So, no need to reschedule it again. 232 // MediaProvider scans are highly unlikely to get killed. But even if it does, we would run 233 // a scan on attachVolume(). But other requests to MediaService may get lost if 234 // MediaProvider process is killed, which would otherwise have been rescheduled by 235 // JobScheduler. 236 // TODO(b/233357418): Fix this by adhering to the protocol of stopping current work when job 237 // scheduler asks 238 return false; 239 } 240 } 241