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