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