• 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.Bundle;
31 import android.os.Trace;
32 import android.os.UserHandle;
33 import android.os.storage.StorageVolume;
34 import android.provider.MediaStore;
35 import android.util.Log;
36 
37 import androidx.core.app.JobIntentService;
38 
39 import com.android.providers.media.util.FileUtils;
40 
41 import java.io.File;
42 import java.io.IOException;
43 
44 public class MediaService extends JobIntentService {
45     private static final int JOB_ID = -300;
46 
47     private static final String ACTION_SCAN_VOLUME
48             = "com.android.providers.media.action.SCAN_VOLUME";
49 
50     private static final String EXTRA_MEDIAVOLUME = "MediaVolume";
51 
52     private static final String EXTRA_SCAN_REASON = "scan_reason";
53 
54 
queueVolumeScan(Context context, MediaVolume volume, int reason)55     public static void queueVolumeScan(Context context, MediaVolume volume, int reason) {
56         Intent intent = new Intent(ACTION_SCAN_VOLUME);
57         intent.putExtra(EXTRA_MEDIAVOLUME, volume) ;
58         intent.putExtra(EXTRA_SCAN_REASON, reason);
59         enqueueWork(context, intent);
60     }
61 
enqueueWork(Context context, Intent work)62     public static void enqueueWork(Context context, Intent work) {
63         enqueueWork(context, MediaService.class, JOB_ID, work);
64     }
65 
66     @Override
onHandleWork(Intent intent)67     protected void onHandleWork(Intent intent) {
68         Trace.beginSection(intent.getAction());
69         if (Log.isLoggable(TAG, Log.INFO)) {
70             Log.i(TAG, "Begin " + intent);
71         }
72         try {
73             switch (intent.getAction()) {
74                 case Intent.ACTION_LOCALE_CHANGED: {
75                     onLocaleChanged();
76                     break;
77                 }
78                 case Intent.ACTION_PACKAGE_FULLY_REMOVED:
79                 case Intent.ACTION_PACKAGE_DATA_CLEARED: {
80                     final String packageName = intent.getData().getSchemeSpecificPart();
81                     onPackageOrphaned(packageName);
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)120     private void onPackageOrphaned(String packageName) {
121         try (ContentProviderClient cpc = getContentResolver()
122                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
123             ((MediaProvider) cpc.getLocalContentProvider()).onPackageOrphaned(packageName);
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         UserHandle owner = volume.getUser();
154         if (owner == null) {
155             // Can happen for the internal volume
156             owner = context.getUser();
157         }
158         // If we're about to scan any external storage, scan internal first
159         // to ensure that we have ringtones ready to roll before a possibly very
160         // long external storage scan
161         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
162             onScanVolume(context, MediaVolume.fromInternal(), reason);
163             RingtoneManager.ensureDefaultRingtones(context);
164         }
165 
166         // Resolve the Uri that we should use for all broadcast intents related
167         // to this volume; we do this once to ensure we can deliver all events
168         // in the situation where a volume is ejected mid-scan
169         final Uri broadcastUri;
170         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
171             broadcastUri = Uri.fromFile(volume.getPath());
172         } else {
173             broadcastUri = null;
174         }
175 
176         try (ContentProviderClient cpc = context.getContentResolver()
177                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
178             final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider());
179             provider.attachVolume(volume, /* validate */ true);
180 
181             final ContentResolver resolver = ContentResolver.wrap(cpc.getLocalContentProvider());
182 
183             ContentValues values = new ContentValues();
184             values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
185             Uri scanUri = resolver.insert(MediaStore.getMediaScannerUri(), values);
186 
187             if (broadcastUri != null) {
188                 context.sendBroadcastAsUser(
189                         new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, broadcastUri), owner);
190             }
191 
192             if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
193                 for (File dir : FileUtils.getVolumeScanPaths(context, volumeName)) {
194                     provider.scanDirectory(dir, reason);
195                 }
196             } else {
197                 provider.scanDirectory(volume.getPath(), reason);
198             }
199 
200             resolver.delete(scanUri, null, null);
201 
202         } finally {
203             if (broadcastUri != null) {
204                 context.sendBroadcastAsUser(
205                         new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, broadcastUri), owner);
206             }
207         }
208     }
209 
onScanFile(Context context, Uri uri)210     private static Uri onScanFile(Context context, Uri uri) throws IOException {
211         final File file = new File(uri.getPath()).getCanonicalFile();
212         try (ContentProviderClient cpc = context.getContentResolver()
213                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
214             final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider());
215             return provider.scanFile(file, REASON_DEMAND);
216         }
217     }
218 }
219