• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                     if (volume.isPublicVolume()) {
95                         recoverPublicVolumeIfNeeded(volume);
96                     }
97                     int reason = intent.getIntExtra(EXTRA_SCAN_REASON, REASON_DEMAND);
98                     onScanVolume(this, volume, reason);
99                     break;
100                 }
101                 default: {
102                     Log.w(TAG, "Unknown intent " + intent);
103                     break;
104                 }
105             }
106         } catch (Exception e) {
107             Log.w(TAG, "Failed operation " + intent, e);
108         } finally {
109             if (Log.isLoggable(TAG, Log.INFO)) {
110                 Log.i(TAG, "End " + intent);
111             }
112             Trace.endSection();
113         }
114     }
115 
onLocaleChanged()116     private void onLocaleChanged() {
117         try (ContentProviderClient cpc = getContentResolver()
118                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
119             ((MediaProvider) cpc.getLocalContentProvider()).onLocaleChanged();
120         }
121     }
122 
onPackageOrphaned(String packageName, int uid)123     private void onPackageOrphaned(String packageName, int uid) {
124         try (ContentProviderClient cpc = getContentResolver()
125                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
126             ((MediaProvider) cpc.getLocalContentProvider()).onPackageOrphaned(packageName, uid);
127         }
128     }
129 
onMediaMountedBroadcast(Context context, Intent intent)130     private static void onMediaMountedBroadcast(Context context, Intent intent)
131             throws IOException {
132         final StorageVolume volume = intent.getParcelableExtra(StorageVolume.EXTRA_STORAGE_VOLUME);
133         if (volume != null) {
134             MediaVolume mediaVolume = MediaVolume.fromStorageVolume(volume);
135             try (ContentProviderClient cpc = context.getContentResolver()
136                     .acquireContentProviderClient(MediaStore.AUTHORITY)) {
137                 if (!((MediaProvider)cpc.getLocalContentProvider()).isVolumeAttached(mediaVolume)) {
138                     // This can happen on some legacy app clone implementations, where the
139                     // framework is modified to send MEDIA_MOUNTED broadcasts for clone volumes
140                     // to u0 MediaProvider; these volumes are not reported through the usual
141                     // volume attach events, so we need to scan them here if they weren't
142                     // attached previously
143                     onScanVolume(context, mediaVolume, REASON_MOUNTED);
144                 } else {
145                     Log.i(TAG, "Volume " + mediaVolume + " already attached");
146                 }
147             }
148         } else {
149             Log.e(TAG, "Couldn't retrieve StorageVolume from intent");
150         }
151     }
152 
recoverPublicVolumeIfNeeded(MediaVolume volume)153     private void recoverPublicVolumeIfNeeded(MediaVolume volume) {
154         try (ContentProviderClient cpc = getContentResolver()
155                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
156             ((MediaProvider) cpc.getLocalContentProvider()).recoverPublicVolume(volume);
157         } catch (Exception e) {
158             Log.e(TAG, "Exception while starting public volume recovery thread", e);
159         }
160     }
161 
onScanVolume(Context context, MediaVolume volume, int reason)162     public static void onScanVolume(Context context, MediaVolume volume, int reason)
163             throws IOException {
164         final String volumeName = volume.getName();
165         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && volume.getPath() == null) {
166             /* This is a very unexpected state and can only ever happen with app-cloned users.
167               In general, MediaVolumes should always be mounted and have a path, however, if the
168               user failed to unlock properly, MediaProvider still gets the volume from the
169               StorageManagerService because MediaProvider is special cased there. See
170               StorageManagerService#getVolumeList. Reference bug: b/207723670. */
171             Log.w(TAG, String.format("Skipping volume scan for %s when volume path is null.",
172                     volumeName));
173             return;
174         }
175         UserHandle owner = volume.getUser();
176         if (owner == null) {
177             // Can happen for the internal volume
178             owner = context.getUser();
179         }
180         // If we're about to scan any external storage, scan internal first
181         // to ensure that we have ringtones ready to roll before a possibly very
182         // long external storage scan
183         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
184             onScanVolume(context, MediaVolume.fromInternal(), reason);
185             RingtoneManager.ensureDefaultRingtones(context);
186         }
187 
188         // Resolve the Uri that we should use for all broadcast intents related
189         // to this volume; we do this once to ensure we can deliver all events
190         // in the situation where a volume is ejected mid-scan
191         final Uri broadcastUri;
192         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
193             broadcastUri = Uri.fromFile(volume.getPath());
194         } else {
195             broadcastUri = null;
196         }
197 
198         try (ContentProviderClient cpc = context.getContentResolver()
199                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
200             final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider());
201             provider.attachVolume(volume, /* validate */ true, /* volumeState */ null);
202 
203             final ContentResolver resolver = ContentResolver.wrap(cpc.getLocalContentProvider());
204 
205             ContentValues values = new ContentValues();
206             values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
207             Uri scanUri = resolver.insert(MediaStore.getMediaScannerUri(), values);
208 
209             if (broadcastUri != null) {
210                 context.sendBroadcastAsUser(
211                         new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, broadcastUri), owner);
212             }
213 
214             if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
215                 for (File dir : FileUtils.getVolumeScanPaths(context, volumeName)) {
216                     provider.scanDirectory(dir, reason);
217                 }
218             } else {
219                 provider.scanDirectory(volume.getPath(), reason);
220             }
221 
222             resolver.delete(scanUri, null, null);
223 
224         } finally {
225             if (broadcastUri != null) {
226                 context.sendBroadcastAsUser(
227                         new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, broadcastUri), owner);
228             }
229         }
230     }
231 
onScanFile(Context context, Uri uri)232     private static Uri onScanFile(Context context, Uri uri) throws IOException {
233         final File file = new File(uri.getPath()).getCanonicalFile();
234         try (ContentProviderClient cpc = context.getContentResolver()
235                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
236             final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider());
237             return provider.scanFile(file, REASON_DEMAND);
238         }
239     }
240 
241     @Override
onStopCurrentWork()242     public boolean onStopCurrentWork() {
243         // Scans are not stopped even if the job is stopped. So, no need to reschedule it again.
244         // MediaProvider scans are highly unlikely to get killed. But even if it does, we would run
245         // a scan on attachVolume(). But other requests to MediaService may get lost if
246         // MediaProvider process is killed, which would otherwise have been rescheduled by
247         // JobScheduler.
248         // TODO(b/233357418): Fix this by adhering to the protocol of stopping current work when job
249         // scheduler asks
250         return false;
251     }
252 }
253