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