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      * Restarts all sessions for {@code userId}.
264      *
265      * Does nothing if {@link #shouldHandle} is {@code false}
266      *
267      * This call blocks and waits for all sessions to be started, however any failures when starting
268      * a session will be ignored.
269      */
onUnlockUser(int userId)270     public void onUnlockUser(int userId) throws ExternalStorageServiceException {
271         Slog.i(TAG, "On user unlock " + userId);
272         if (shouldHandle(null) && userId == 0) {
273             initExternalStorageServiceComponent();
274         }
275     }
276 
277     /**
278      * Called when a user is in the process is being stopped.
279      *
280      * Does nothing if {@link #shouldHandle} is {@code false}
281      *
282      * This call removes all sessions for the user that is being stopped;
283      * this will make sure that we don't rebind to the service needlessly.
284      */
onUserStopping(int userId)285     public void onUserStopping(int userId) {
286         if (!shouldHandle(null)) {
287             return;
288         }
289         StorageUserConnection connection = null;
290         synchronized (mLock) {
291             connection = mConnections.get(userId);
292         }
293 
294         if (connection != null) {
295             Slog.i(TAG, "Removing all sessions for user: " + userId);
296             connection.removeAllSessions();
297         } else {
298             Slog.w(TAG, "No connection found for user: " + userId);
299         }
300     }
301 
302     /**
303      * Resets all sessions for all users and waits for exit. This may kill the
304      * {@link ExternalStorageservice} for a user if necessary to ensure all state has been reset.
305      *
306      * Does nothing if {@link #shouldHandle} is {@code false}
307      **/
onReset(IVold vold, Runnable resetHandlerRunnable)308     public void onReset(IVold vold, Runnable resetHandlerRunnable) {
309         if (!shouldHandle(null)) {
310             return;
311         }
312 
313         SparseArray<StorageUserConnection> connections = new SparseArray();
314         synchronized (mLock) {
315             mIsResetting = true;
316             Slog.i(TAG, "Started resetting external storage service...");
317             for (int i = 0; i < mConnections.size(); i++) {
318                 connections.put(mConnections.keyAt(i), mConnections.valueAt(i));
319             }
320         }
321 
322         for (int i = 0; i < connections.size(); i++) {
323             StorageUserConnection connection = connections.valueAt(i);
324             for (String sessionId : connection.getAllSessionIds()) {
325                 try {
326                     Slog.i(TAG, "Unmounting " + sessionId);
327                     vold.unmount(sessionId);
328                     Slog.i(TAG, "Unmounted " + sessionId);
329                 } catch (ServiceSpecificException | RemoteException e) {
330                     // TODO(b/140025078): Hard reset vold?
331                     Slog.e(TAG, "Failed to unmount volume: " + sessionId, e);
332                 }
333 
334                 try {
335                     Slog.i(TAG, "Exiting " + sessionId);
336                     connection.removeSessionAndWait(sessionId);
337                     Slog.i(TAG, "Exited " + sessionId);
338                 } catch (IllegalStateException | ExternalStorageServiceException e) {
339                     Slog.e(TAG, "Failed to exit session: " + sessionId
340                             + ". Killing MediaProvider...", e);
341                     // If we failed to confirm the session exited, it is risky to proceed
342                     // We kill the ExternalStorageService as a last resort
343                     killExternalStorageService(connections.keyAt(i));
344                     break;
345                 }
346             }
347             connection.close();
348         }
349 
350         resetHandlerRunnable.run();
351         synchronized (mLock) {
352             mConnections.clear();
353             mIsResetting = false;
354             Slog.i(TAG, "Finished resetting external storage service");
355         }
356     }
357 
initExternalStorageServiceComponent()358     private void initExternalStorageServiceComponent() throws ExternalStorageServiceException {
359         Slog.i(TAG, "Initialialising...");
360         ProviderInfo provider = mContext.getPackageManager().resolveContentProvider(
361                 MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
362                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
363                 | PackageManager.MATCH_SYSTEM_ONLY);
364         if (provider == null) {
365             throw new ExternalStorageServiceException("No valid MediaStore provider found");
366         }
367 
368         mExternalStorageServicePackageName = provider.applicationInfo.packageName;
369         mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid);
370 
371         Intent intent = new Intent(ExternalStorageService.SERVICE_INTERFACE);
372         intent.setPackage(mExternalStorageServicePackageName);
373         ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
374                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
375         if (resolveInfo == null || resolveInfo.serviceInfo == null) {
376             throw new ExternalStorageServiceException(
377                     "No valid ExternalStorageService component found");
378         }
379 
380         ServiceInfo serviceInfo = resolveInfo.serviceInfo;
381         ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
382         if (!Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE
383                 .equals(serviceInfo.permission)) {
384             throw new ExternalStorageServiceException(name.flattenToShortString()
385                     + " does not require permission "
386                     + Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE);
387         }
388 
389         mExternalStorageServiceComponent = name;
390     }
391 
392     /** Returns the {@link ExternalStorageService} component name. */
393     @Nullable
getExternalStorageServiceComponentName()394     public ComponentName getExternalStorageServiceComponentName() {
395         return mExternalStorageServiceComponent;
396     }
397 
398     /**
399      * Notify the controller that an app with {@code uid} and {@code tid} is blocked on an IO
400      * request on {@code volumeUuid} for {@code reason}.
401      *
402      * This blocked state can be queried with {@link #isAppIoBlocked}
403      *
404      * @hide
405      */
notifyAppIoBlocked(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)406     public void notifyAppIoBlocked(String volumeUuid, int uid, int tid,
407             @StorageManager.AppIoBlockedReason int reason) {
408         final int userId = UserHandle.getUserId(uid);
409         final StorageUserConnection connection;
410         synchronized (mLock) {
411             connection = mConnections.get(userId);
412         }
413 
414         if (connection != null) {
415             connection.notifyAppIoBlocked(volumeUuid, uid, tid, reason);
416         }
417     }
418 
419     /**
420      * Notify the controller that an app with {@code uid} and {@code tid} has resmed a previously
421      * blocked IO request on {@code volumeUuid} for {@code reason}.
422      *
423      * All app IO will be automatically marked as unblocked if {@code volumeUuid} is unmounted.
424      */
notifyAppIoResumed(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)425     public void notifyAppIoResumed(String volumeUuid, int uid, int tid,
426             @StorageManager.AppIoBlockedReason int reason) {
427         final int userId = UserHandle.getUserId(uid);
428         final StorageUserConnection connection;
429         synchronized (mLock) {
430             connection = mConnections.get(userId);
431         }
432 
433         if (connection != null) {
434             connection.notifyAppIoResumed(volumeUuid, uid, tid, reason);
435         }
436     }
437 
438     /** Returns {@code true} if {@code uid} is blocked on IO, {@code false} otherwise */
isAppIoBlocked(int uid)439     public boolean isAppIoBlocked(int uid) {
440         final int userId = UserHandle.getUserId(uid);
441         final StorageUserConnection connection;
442         synchronized (mLock) {
443             connection = mConnections.get(userId);
444         }
445 
446         if (connection != null) {
447             return connection.isAppIoBlocked(uid);
448         }
449         return false;
450     }
451 
killExternalStorageService(int userId)452     private void killExternalStorageService(int userId) {
453         IActivityManager am = ActivityManager.getService();
454         try {
455             am.killApplication(mExternalStorageServicePackageName, mExternalStorageServiceAppId,
456                     userId, "storage_session_controller reset");
457         } catch (RemoteException e) {
458             Slog.i(TAG, "Failed to kill the ExtenalStorageService for user " + userId);
459         }
460     }
461 
462     /**
463      * Returns {@code true} if {@code vol} is an emulated or visible public volume,
464      * {@code false} otherwise
465      **/
isEmulatedOrPublic(VolumeInfo vol)466     public static boolean isEmulatedOrPublic(VolumeInfo vol) {
467         return vol.type == VolumeInfo.TYPE_EMULATED
468                 || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible());
469     }
470 
471     /** Exception thrown when communication with the {@link ExternalStorageService} fails. */
472     public static class ExternalStorageServiceException extends Exception {
ExternalStorageServiceException(Throwable cause)473         public ExternalStorageServiceException(Throwable cause) {
474             super(cause);
475         }
476 
ExternalStorageServiceException(String message)477         public ExternalStorageServiceException(String message) {
478             super(message);
479         }
480 
ExternalStorageServiceException(String message, Throwable cause)481         public ExternalStorageServiceException(String message, Throwable cause) {
482             super(message, cause);
483         }
484     }
485 
isSupportedVolume(VolumeInfo vol)486     private static boolean isSupportedVolume(VolumeInfo vol) {
487         return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB;
488     }
489 
shouldHandle(@ullable VolumeInfo vol)490     private boolean shouldHandle(@Nullable VolumeInfo vol) {
491         return !mIsResetting && (vol == null || isSupportedVolume(vol));
492     }
493 }
494