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