• 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.server.storage;
18 
19 import android.Manifest;
20 import android.annotation.Nullable;
21 import android.app.ActivityManager;
22 import android.app.IActivityManager;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ProviderInfo;
28 import android.content.pm.ResolveInfo;
29 import android.content.pm.ServiceInfo;
30 import android.content.pm.UserInfo;
31 import android.os.IVold;
32 import android.os.ParcelFileDescriptor;
33 import android.os.RemoteException;
34 import android.os.ServiceSpecificException;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.os.storage.StorageManager;
38 import android.os.storage.StorageVolume;
39 import android.os.storage.VolumeInfo;
40 import android.provider.MediaStore;
41 import android.service.storage.ExternalStorageService;
42 import android.util.Slog;
43 import android.util.SparseArray;
44 
45 import com.android.internal.annotations.GuardedBy;
46 
47 import java.util.Objects;
48 
49 /**
50  * Controls storage sessions for users initiated by the {@link StorageManagerService}.
51  * Each user on the device will be represented by a {@link StorageUserConnection}.
52  */
53 public final class StorageSessionController {
54     private static final String TAG = "StorageSessionController";
55 
56     private final Object mLock = new Object();
57     private final Context mContext;
58     private final UserManager mUserManager;
59     @GuardedBy("mLock")
60     private final SparseArray<StorageUserConnection> mConnections = new SparseArray<>();
61 
62     private volatile ComponentName mExternalStorageServiceComponent;
63     private volatile String mExternalStorageServicePackageName;
64     private volatile int mExternalStorageServiceAppId;
65     private volatile boolean mIsResetting;
66 
StorageSessionController(Context context)67     public StorageSessionController(Context context) {
68         mContext = Objects.requireNonNull(context);
69         mUserManager = mContext.getSystemService(UserManager.class);
70     }
71 
72     /**
73      * Returns userId for the volume to be used in the StorageUserConnection.
74      * If the user is a clone profile, it will use the same connection
75      * as the parent user, and hence this method returns the parent's userId. Else, it returns the
76      * volume's mountUserId
77      * @param vol for which the storage session has to be started
78      * @return userId for connection for this volume
79      */
getConnectionUserIdForVolume(VolumeInfo vol)80     public int getConnectionUserIdForVolume(VolumeInfo vol) {
81         final Context volumeUserContext = mContext.createContextAsUser(
82                 UserHandle.of(vol.mountUserId), 0);
83         boolean isMediaSharedWithParent = volumeUserContext.getSystemService(
84                 UserManager.class).isMediaSharedWithParent();
85 
86         UserInfo userInfo = mUserManager.getUserInfo(vol.mountUserId);
87         if (userInfo != null && isMediaSharedWithParent) {
88             // Clones use the same connection as their parent
89             return userInfo.profileGroupId;
90         } else {
91             return vol.mountUserId;
92         }
93     }
94 
95     /**
96      * Creates and starts a storage session associated with {@code deviceFd} for {@code vol}.
97      * Sessions can be started with {@link #onVolumeReady} and removed with {@link #onVolumeUnmount}
98      * or {@link #onVolumeRemove}.
99      *
100      * Throws an {@link IllegalStateException} if a session for {@code vol} has already been created
101      *
102      * Does nothing if {@link #shouldHandle} is {@code false}
103      *
104      * Blocks until the session is started or fails
105      *
106      * @throws ExternalStorageServiceException if the session fails to start
107      * @throws IllegalStateException if a session has already been created for {@code vol}
108      */
onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol)109     public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol)
110             throws ExternalStorageServiceException {
111         if (!shouldHandle(vol)) {
112             return;
113         }
114 
115         Slog.i(TAG, "On volume mount " + vol);
116 
117         String sessionId = vol.getId();
118         int userId = getConnectionUserIdForVolume(vol);
119 
120         StorageUserConnection connection = null;
121         synchronized (mLock) {
122             connection = mConnections.get(userId);
123             if (connection == null) {
124                 Slog.i(TAG, "Creating connection for user: " + userId);
125                 connection = new StorageUserConnection(mContext, userId, this);
126                 mConnections.put(userId, connection);
127             }
128             Slog.i(TAG, "Creating and starting session with id: " + sessionId);
129             connection.startSession(sessionId, deviceFd, vol.getPath().getPath(),
130                     vol.getInternalPath().getPath());
131         }
132     }
133 
134     /**
135      * Notifies the Storage Service that volume state for {@code vol} is changed.
136      * A session may already be created for this volume if it is mounted before or the volume state
137      * has changed to mounted.
138      *
139      * Does nothing if {@link #shouldHandle} is {@code false}
140      *
141      * Blocks until the Storage Service processes/scans the volume or fails in doing so.
142      *
143      * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService
144      */
notifyVolumeStateChanged(VolumeInfo vol)145     public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException {
146         if (!shouldHandle(vol)) {
147             return;
148         }
149         String sessionId = vol.getId();
150         int connectionUserId = getConnectionUserIdForVolume(vol);
151 
152         StorageUserConnection connection = null;
153         synchronized (mLock) {
154             connection = mConnections.get(connectionUserId);
155             if (connection != null) {
156                 Slog.i(TAG, "Notifying volume state changed for session with id: " + sessionId);
157                 connection.notifyVolumeStateChanged(sessionId,
158                         vol.buildStorageVolume(mContext, vol.getMountUserId(), false));
159             } else {
160                 Slog.w(TAG, "No available storage user connection for userId : "
161                         + connectionUserId);
162             }
163         }
164     }
165 
166     /**
167      * Frees any cache held by ExternalStorageService.
168      *
169      * <p> Blocks until the service frees the cache or fails in doing so.
170      *
171      * @param volumeUuid uuid of the {@link StorageVolume} from which cache needs to be freed
172      * @param bytes number of bytes which need to be freed
173      * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService
174      */
freeCache(String volumeUuid, long bytes)175     public void freeCache(String volumeUuid, long bytes)
176             throws ExternalStorageServiceException {
177         synchronized (mLock) {
178             int size = mConnections.size();
179             for (int i = 0; i < size; i++) {
180                 int key = mConnections.keyAt(i);
181                 StorageUserConnection connection = mConnections.get(key);
182                 if (connection != null) {
183                     connection.freeCache(volumeUuid, bytes);
184                 }
185             }
186         }
187     }
188 
189     /**
190      * Called when {@code packageName} is about to ANR
191      *
192      * @return ANR dialog delay in milliseconds
193      */
notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)194     public void notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)
195             throws ExternalStorageServiceException {
196         final int userId = UserHandle.getUserId(uid);
197         final StorageUserConnection connection;
198         synchronized (mLock) {
199             connection = mConnections.get(userId);
200         }
201 
202         if (connection != null) {
203             connection.notifyAnrDelayStarted(packageName, uid, tid, reason);
204         }
205     }
206 
207     /**
208      * Removes and returns the {@link StorageUserConnection} for {@code vol}.
209      *
210      * Does nothing if {@link #shouldHandle} is {@code false}
211      *
212      * @return the connection that was removed or {@code null} if nothing was removed
213      */
214     @Nullable
onVolumeRemove(VolumeInfo vol)215     public StorageUserConnection onVolumeRemove(VolumeInfo vol) {
216         if (!shouldHandle(vol)) {
217             return null;
218         }
219 
220         Slog.i(TAG, "On volume remove " + vol);
221         String sessionId = vol.getId();
222         int userId = getConnectionUserIdForVolume(vol);
223 
224         synchronized (mLock) {
225             StorageUserConnection connection = mConnections.get(userId);
226             if (connection != null) {
227                 Slog.i(TAG, "Removed session for vol with id: " + sessionId);
228                 connection.removeSession(sessionId);
229                 return connection;
230             } else {
231                 Slog.w(TAG, "Session already removed for vol with id: " + sessionId);
232                 return null;
233             }
234         }
235     }
236 
237 
238     /**
239      * Removes a storage session for {@code vol} and waits for exit.
240      *
241      * Does nothing if {@link #shouldHandle} is {@code false}
242      *
243      * Any errors are ignored
244      *
245      * Call {@link #onVolumeRemove} to remove the connection without waiting for exit
246      */
onVolumeUnmount(VolumeInfo vol)247     public void onVolumeUnmount(VolumeInfo vol) {
248         StorageUserConnection connection = onVolumeRemove(vol);
249 
250         Slog.i(TAG, "On volume unmount " + vol);
251         if (connection != null) {
252             String sessionId = vol.getId();
253 
254             try {
255                 connection.removeSessionAndWait(sessionId);
256             } catch (ExternalStorageServiceException e) {
257                 Slog.e(TAG, "Failed to end session for vol with id: " + sessionId, e);
258             }
259         }
260     }
261 
262     /**
263      * Makes sure we initialize the ExternalStorageService component.
264      */
onUnlockUser(int userId)265     public void onUnlockUser(int userId) throws ExternalStorageServiceException {
266         Slog.i(TAG, "On user unlock " + userId);
267         if (userId == 0) {
268             initExternalStorageServiceComponent();
269         }
270     }
271 
272     /**
273      * Called when a user is in the process is being stopped.
274      *
275      * Does nothing if {@link #shouldHandle} is {@code false}
276      *
277      * This call removes all sessions for the user that is being stopped;
278      * this will make sure that we don't rebind to the service needlessly.
279      */
onUserStopping(int userId)280     public void onUserStopping(int userId) {
281         if (!shouldHandle(null)) {
282             return;
283         }
284         StorageUserConnection connection = null;
285         synchronized (mLock) {
286             connection = mConnections.get(userId);
287         }
288 
289         if (connection != null) {
290             Slog.i(TAG, "Removing all sessions for user: " + userId);
291             connection.removeAllSessions();
292         } else {
293             Slog.w(TAG, "No connection found for user: " + userId);
294         }
295     }
296 
297     /**
298      * Resets all sessions for all users and waits for exit. This may kill the
299      * {@link ExternalStorageservice} for a user if necessary to ensure all state has been reset.
300      *
301      * Does nothing if {@link #shouldHandle} is {@code false}
302      **/
onReset(IVold vold, Runnable resetHandlerRunnable)303     public void onReset(IVold vold, Runnable resetHandlerRunnable) {
304         if (!shouldHandle(null)) {
305             return;
306         }
307 
308         SparseArray<StorageUserConnection> connections = new SparseArray();
309         synchronized (mLock) {
310             mIsResetting = true;
311             Slog.i(TAG, "Started resetting external storage service...");
312             for (int i = 0; i < mConnections.size(); i++) {
313                 connections.put(mConnections.keyAt(i), mConnections.valueAt(i));
314             }
315         }
316 
317         for (int i = 0; i < connections.size(); i++) {
318             StorageUserConnection connection = connections.valueAt(i);
319             for (String sessionId : connection.getAllSessionIds()) {
320                 try {
321                     Slog.i(TAG, "Unmounting " + sessionId);
322                     vold.unmount(sessionId);
323                     Slog.i(TAG, "Unmounted " + sessionId);
324                 } catch (ServiceSpecificException | RemoteException e) {
325                     // TODO(b/140025078): Hard reset vold?
326                     Slog.e(TAG, "Failed to unmount volume: " + sessionId, e);
327                 }
328 
329                 try {
330                     Slog.i(TAG, "Exiting " + sessionId);
331                     connection.removeSessionAndWait(sessionId);
332                     Slog.i(TAG, "Exited " + sessionId);
333                 } catch (IllegalStateException | ExternalStorageServiceException e) {
334                     Slog.e(TAG, "Failed to exit session: " + sessionId
335                             + ". Killing MediaProvider...", e);
336                     // If we failed to confirm the session exited, it is risky to proceed
337                     // We kill the ExternalStorageService as a last resort
338                     killExternalStorageService(connections.keyAt(i));
339                     break;
340                 }
341             }
342             connection.close();
343         }
344 
345         resetHandlerRunnable.run();
346         synchronized (mLock) {
347             mConnections.clear();
348             mIsResetting = false;
349             Slog.i(TAG, "Finished resetting external storage service");
350         }
351     }
352 
initExternalStorageServiceComponent()353     private void initExternalStorageServiceComponent() throws ExternalStorageServiceException {
354         Slog.i(TAG, "Initialialising...");
355         ProviderInfo provider = mContext.getPackageManager().resolveContentProvider(
356                 MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
357                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
358                 | PackageManager.MATCH_SYSTEM_ONLY);
359         if (provider == null) {
360             throw new ExternalStorageServiceException("No valid MediaStore provider found");
361         }
362 
363         mExternalStorageServicePackageName = provider.applicationInfo.packageName;
364         mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid);
365 
366         ServiceInfo serviceInfo = resolveExternalStorageServiceAsUser(UserHandle.USER_SYSTEM);
367         if (serviceInfo == null) {
368             throw new ExternalStorageServiceException(
369                     "No valid ExternalStorageService component found");
370         }
371 
372         ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
373         if (!Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE
374                 .equals(serviceInfo.permission)) {
375             throw new ExternalStorageServiceException(name.flattenToShortString()
376                     + " does not require permission "
377                     + Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE);
378         }
379 
380         mExternalStorageServiceComponent = name;
381     }
382 
383     /** Returns the {@link ExternalStorageService} component name. */
384     @Nullable
getExternalStorageServiceComponentName()385     public ComponentName getExternalStorageServiceComponentName() {
386         return mExternalStorageServiceComponent;
387     }
388 
389     /**
390      * Notify the controller that an app with {@code uid} and {@code tid} is blocked on an IO
391      * request on {@code volumeUuid} for {@code reason}.
392      *
393      * This blocked state can be queried with {@link #isAppIoBlocked}
394      *
395      * @hide
396      */
notifyAppIoBlocked(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)397     public void notifyAppIoBlocked(String volumeUuid, int uid, int tid,
398             @StorageManager.AppIoBlockedReason int reason) {
399         final int userId = UserHandle.getUserId(uid);
400         final StorageUserConnection connection;
401         synchronized (mLock) {
402             connection = mConnections.get(userId);
403         }
404 
405         if (connection != null) {
406             connection.notifyAppIoBlocked(volumeUuid, uid, tid, reason);
407         }
408     }
409 
410     /**
411      * Notify the controller that an app with {@code uid} and {@code tid} has resmed a previously
412      * blocked IO request on {@code volumeUuid} for {@code reason}.
413      *
414      * All app IO will be automatically marked as unblocked if {@code volumeUuid} is unmounted.
415      */
notifyAppIoResumed(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)416     public void notifyAppIoResumed(String volumeUuid, int uid, int tid,
417             @StorageManager.AppIoBlockedReason int reason) {
418         final int userId = UserHandle.getUserId(uid);
419         final StorageUserConnection connection;
420         synchronized (mLock) {
421             connection = mConnections.get(userId);
422         }
423 
424         if (connection != null) {
425             connection.notifyAppIoResumed(volumeUuid, uid, tid, reason);
426         }
427     }
428 
429     /** Returns {@code true} if {@code uid} is blocked on IO, {@code false} otherwise */
isAppIoBlocked(int uid)430     public boolean isAppIoBlocked(int uid) {
431         final int userId = UserHandle.getUserId(uid);
432         final StorageUserConnection connection;
433         synchronized (mLock) {
434             connection = mConnections.get(userId);
435         }
436 
437         if (connection != null) {
438             return connection.isAppIoBlocked(uid);
439         }
440         return false;
441     }
442 
killExternalStorageService(int userId)443     private void killExternalStorageService(int userId) {
444         IActivityManager am = ActivityManager.getService();
445         try {
446             am.killApplication(mExternalStorageServicePackageName, mExternalStorageServiceAppId,
447                     userId, "storage_session_controller reset");
448         } catch (RemoteException e) {
449             Slog.i(TAG, "Failed to kill the ExtenalStorageService for user " + userId);
450         }
451     }
452 
453     /**
454      * Returns {@code true} if {@code vol} is an emulated or visible public volume,
455      * {@code false} otherwise
456      **/
isEmulatedOrPublic(VolumeInfo vol)457     public static boolean isEmulatedOrPublic(VolumeInfo vol) {
458         return vol.type == VolumeInfo.TYPE_EMULATED
459                 || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible());
460     }
461 
462     /** Exception thrown when communication with the {@link ExternalStorageService} fails. */
463     public static class ExternalStorageServiceException extends Exception {
ExternalStorageServiceException(Throwable cause)464         public ExternalStorageServiceException(Throwable cause) {
465             super(cause);
466         }
467 
ExternalStorageServiceException(String message)468         public ExternalStorageServiceException(String message) {
469             super(message);
470         }
471 
ExternalStorageServiceException(String message, Throwable cause)472         public ExternalStorageServiceException(String message, Throwable cause) {
473             super(message, cause);
474         }
475     }
476 
isSupportedVolume(VolumeInfo vol)477     private static boolean isSupportedVolume(VolumeInfo vol) {
478         return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB;
479     }
480 
shouldHandle(@ullable VolumeInfo vol)481     private boolean shouldHandle(@Nullable VolumeInfo vol) {
482         return !mIsResetting && (vol == null || isSupportedVolume(vol));
483     }
484 
485     /**
486      * Returns {@code true} if the given user supports external storage,
487      * {@code false} otherwise.
488      */
supportsExternalStorage(int userId)489     public boolean supportsExternalStorage(int userId) {
490         return resolveExternalStorageServiceAsUser(userId) != null;
491     }
492 
resolveExternalStorageServiceAsUser(int userId)493     private ServiceInfo resolveExternalStorageServiceAsUser(int userId) {
494         Intent intent = new Intent(ExternalStorageService.SERVICE_INTERFACE);
495         intent.setPackage(mExternalStorageServicePackageName);
496         ResolveInfo resolveInfo = mContext.getPackageManager().resolveServiceAsUser(intent,
497                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId);
498         if (resolveInfo == null) {
499             return null;
500         }
501 
502         return resolveInfo.serviceInfo;
503     }
504 }
505