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.os.IVold;
31 import android.os.ParcelFileDescriptor;
32 import android.os.RemoteException;
33 import android.os.ServiceSpecificException;
34 import android.os.UserHandle;
35 import android.os.storage.VolumeInfo;
36 import android.provider.MediaStore;
37 import android.service.storage.ExternalStorageService;
38 import android.util.Slog;
39 import android.util.SparseArray;
40 
41 import com.android.internal.annotations.GuardedBy;
42 
43 import java.util.Objects;
44 
45 /**
46  * Controls storage sessions for users initiated by the {@link StorageManagerService}.
47  * Each user on the device will be represented by a {@link StorageUserConnection}.
48  */
49 public final class StorageSessionController {
50     private static final String TAG = "StorageSessionController";
51 
52     private final Object mLock = new Object();
53     private final Context mContext;
54     @GuardedBy("mLock")
55     private final SparseArray<StorageUserConnection> mConnections = new SparseArray<>();
56     private final boolean mIsFuseEnabled;
57 
58     private volatile ComponentName mExternalStorageServiceComponent;
59     private volatile String mExternalStorageServicePackageName;
60     private volatile int mExternalStorageServiceAppId;
61     private volatile boolean mIsResetting;
62 
StorageSessionController(Context context, boolean isFuseEnabled)63     public StorageSessionController(Context context, boolean isFuseEnabled) {
64         mContext = Objects.requireNonNull(context);
65         mIsFuseEnabled = isFuseEnabled;
66     }
67 
68     /**
69      * Creates and starts a storage session associated with {@code deviceFd} for {@code vol}.
70      * Sessions can be started with {@link #onVolumeReady} and removed with {@link #onVolumeUnmount}
71      * or {@link #onVolumeRemove}.
72      *
73      * Throws an {@link IllegalStateException} if a session for {@code vol} has already been created
74      *
75      * Does nothing if {@link #shouldHandle} is {@code false}
76      *
77      * Blocks until the session is started or fails
78      *
79      * @throws ExternalStorageServiceException if the session fails to start
80      * @throws IllegalStateException if a session has already been created for {@code vol}
81      */
onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol)82     public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol)
83             throws ExternalStorageServiceException {
84         if (!shouldHandle(vol)) {
85             return;
86         }
87 
88         Slog.i(TAG, "On volume mount " + vol);
89 
90         String sessionId = vol.getId();
91         int userId = vol.getMountUserId();
92 
93         StorageUserConnection connection = null;
94         synchronized (mLock) {
95             connection = mConnections.get(userId);
96             if (connection == null) {
97                 Slog.i(TAG, "Creating connection for user: " + userId);
98                 connection = new StorageUserConnection(mContext, userId, this);
99                 mConnections.put(userId, connection);
100             }
101             Slog.i(TAG, "Creating and starting session with id: " + sessionId);
102             connection.startSession(sessionId, deviceFd, vol.getPath().getPath(),
103                     vol.getInternalPath().getPath());
104         }
105     }
106 
107     /**
108      * Notifies the Storage Service that volume state for {@code vol} is changed.
109      * A session may already be created for this volume if it is mounted before or the volume state
110      * has changed to mounted.
111      *
112      * Does nothing if {@link #shouldHandle} is {@code false}
113      *
114      * Blocks until the Storage Service processes/scans the volume or fails in doing so.
115      *
116      * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService
117      */
notifyVolumeStateChanged(VolumeInfo vol)118     public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException {
119         if (!shouldHandle(vol)) {
120             return;
121         }
122         String sessionId = vol.getId();
123         int userId = vol.getMountUserId();
124 
125         StorageUserConnection connection = null;
126         synchronized (mLock) {
127             connection = mConnections.get(userId);
128             if (connection != null) {
129                 Slog.i(TAG, "Notifying volume state changed for session with id: " + sessionId);
130                 connection.notifyVolumeStateChanged(sessionId,
131                         vol.buildStorageVolume(mContext, userId, false));
132             } else {
133                 Slog.w(TAG, "No available storage user connection for userId : " + userId);
134             }
135         }
136     }
137 
138 
139     /**
140      * Removes and returns the {@link StorageUserConnection} for {@code vol}.
141      *
142      * Does nothing if {@link #shouldHandle} is {@code false}
143      *
144      * @return the connection that was removed or {@code null} if nothing was removed
145      */
146     @Nullable
onVolumeRemove(VolumeInfo vol)147     public StorageUserConnection onVolumeRemove(VolumeInfo vol) {
148         if (!shouldHandle(vol)) {
149             return null;
150         }
151 
152         Slog.i(TAG, "On volume remove " + vol);
153         String sessionId = vol.getId();
154         int userId = vol.getMountUserId();
155 
156         synchronized (mLock) {
157             StorageUserConnection connection = mConnections.get(userId);
158             if (connection != null) {
159                 Slog.i(TAG, "Removed session for vol with id: " + sessionId);
160                 connection.removeSession(sessionId);
161                 return connection;
162             } else {
163                 Slog.w(TAG, "Session already removed for vol with id: " + sessionId);
164                 return null;
165             }
166         }
167     }
168 
169 
170     /**
171      * Removes a storage session for {@code vol} and waits for exit.
172      *
173      * Does nothing if {@link #shouldHandle} is {@code false}
174      *
175      * Any errors are ignored
176      *
177      * Call {@link #onVolumeRemove} to remove the connection without waiting for exit
178      */
onVolumeUnmount(VolumeInfo vol)179     public void onVolumeUnmount(VolumeInfo vol) {
180         StorageUserConnection connection = onVolumeRemove(vol);
181 
182         Slog.i(TAG, "On volume unmount " + vol);
183         if (connection != null) {
184             String sessionId = vol.getId();
185 
186             try {
187                 connection.removeSessionAndWait(sessionId);
188             } catch (ExternalStorageServiceException e) {
189                 Slog.e(TAG, "Failed to end session for vol with id: " + sessionId, e);
190             }
191         }
192     }
193 
194     /**
195      * Restarts all sessions for {@code userId}.
196      *
197      * Does nothing if {@link #shouldHandle} is {@code false}
198      *
199      * This call blocks and waits for all sessions to be started, however any failures when starting
200      * a session will be ignored.
201      */
onUnlockUser(int userId)202     public void onUnlockUser(int userId) throws ExternalStorageServiceException {
203         Slog.i(TAG, "On user unlock " + userId);
204         if (shouldHandle(null) && userId == 0) {
205             initExternalStorageServiceComponent();
206         }
207     }
208 
209     /**
210      * Called when a user is in the process is being stopped.
211      *
212      * Does nothing if {@link #shouldHandle} is {@code false}
213      *
214      * This call removes all sessions for the user that is being stopped;
215      * this will make sure that we don't rebind to the service needlessly.
216      */
onUserStopping(int userId)217     public void onUserStopping(int userId) {
218         if (!shouldHandle(null)) {
219             return;
220         }
221         StorageUserConnection connection = null;
222         synchronized (mLock) {
223             connection = mConnections.get(userId);
224         }
225 
226         if (connection != null) {
227             Slog.i(TAG, "Removing all sessions for user: " + userId);
228             connection.removeAllSessions();
229         } else {
230             Slog.w(TAG, "No connection found for user: " + userId);
231         }
232     }
233 
234     /**
235      * Resets all sessions for all users and waits for exit. This may kill the
236      * {@link ExternalStorageservice} for a user if necessary to ensure all state has been reset.
237      *
238      * Does nothing if {@link #shouldHandle} is {@code false}
239      **/
onReset(IVold vold, Runnable resetHandlerRunnable)240     public void onReset(IVold vold, Runnable resetHandlerRunnable) {
241         if (!shouldHandle(null)) {
242             return;
243         }
244 
245         SparseArray<StorageUserConnection> connections = new SparseArray();
246         synchronized (mLock) {
247             mIsResetting = true;
248             Slog.i(TAG, "Started resetting external storage service...");
249             for (int i = 0; i < mConnections.size(); i++) {
250                 connections.put(mConnections.keyAt(i), mConnections.valueAt(i));
251             }
252         }
253 
254         for (int i = 0; i < connections.size(); i++) {
255             StorageUserConnection connection = connections.valueAt(i);
256             for (String sessionId : connection.getAllSessionIds()) {
257                 try {
258                     Slog.i(TAG, "Unmounting " + sessionId);
259                     vold.unmount(sessionId);
260                     Slog.i(TAG, "Unmounted " + sessionId);
261                 } catch (ServiceSpecificException | RemoteException e) {
262                     // TODO(b/140025078): Hard reset vold?
263                     Slog.e(TAG, "Failed to unmount volume: " + sessionId, e);
264                 }
265 
266                 try {
267                     Slog.i(TAG, "Exiting " + sessionId);
268                     connection.removeSessionAndWait(sessionId);
269                     Slog.i(TAG, "Exited " + sessionId);
270                 } catch (IllegalStateException | ExternalStorageServiceException e) {
271                     Slog.e(TAG, "Failed to exit session: " + sessionId
272                             + ". Killing MediaProvider...", e);
273                     // If we failed to confirm the session exited, it is risky to proceed
274                     // We kill the ExternalStorageService as a last resort
275                     killExternalStorageService(connections.keyAt(i));
276                     break;
277                 }
278             }
279             connection.close();
280         }
281 
282         resetHandlerRunnable.run();
283         synchronized (mLock) {
284             mConnections.clear();
285             mIsResetting = false;
286             Slog.i(TAG, "Finished resetting external storage service");
287         }
288     }
289 
initExternalStorageServiceComponent()290     private void initExternalStorageServiceComponent() throws ExternalStorageServiceException {
291         Slog.i(TAG, "Initialialising...");
292         ProviderInfo provider = mContext.getPackageManager().resolveContentProvider(
293                 MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
294                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
295                 | PackageManager.MATCH_SYSTEM_ONLY);
296         if (provider == null) {
297             throw new ExternalStorageServiceException("No valid MediaStore provider found");
298         }
299 
300         mExternalStorageServicePackageName = provider.applicationInfo.packageName;
301         mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid);
302 
303         Intent intent = new Intent(ExternalStorageService.SERVICE_INTERFACE);
304         intent.setPackage(mExternalStorageServicePackageName);
305         ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
306                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
307         if (resolveInfo == null || resolveInfo.serviceInfo == null) {
308             throw new ExternalStorageServiceException(
309                     "No valid ExternalStorageService component found");
310         }
311 
312         ServiceInfo serviceInfo = resolveInfo.serviceInfo;
313         ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
314         if (!Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE
315                 .equals(serviceInfo.permission)) {
316             throw new ExternalStorageServiceException(name.flattenToShortString()
317                     + " does not require permission "
318                     + Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE);
319         }
320 
321         mExternalStorageServiceComponent = name;
322     }
323 
324     /** Returns the {@link ExternalStorageService} component name. */
325     @Nullable
getExternalStorageServiceComponentName()326     public ComponentName getExternalStorageServiceComponentName() {
327         return mExternalStorageServiceComponent;
328     }
329 
killExternalStorageService(int userId)330     private void killExternalStorageService(int userId) {
331         IActivityManager am = ActivityManager.getService();
332         try {
333             am.killApplication(mExternalStorageServicePackageName, mExternalStorageServiceAppId,
334                     userId, "storage_session_controller reset");
335         } catch (RemoteException e) {
336             Slog.i(TAG, "Failed to kill the ExtenalStorageService for user " + userId);
337         }
338     }
339 
340     /**
341      * Returns {@code true} if {@code vol} is an emulated or public volume,
342      * {@code false} otherwise
343      **/
isEmulatedOrPublic(VolumeInfo vol)344     public static boolean isEmulatedOrPublic(VolumeInfo vol) {
345         return vol.type == VolumeInfo.TYPE_EMULATED || vol.type == VolumeInfo.TYPE_PUBLIC;
346     }
347 
348     /** Exception thrown when communication with the {@link ExternalStorageService} fails. */
349     public static class ExternalStorageServiceException extends Exception {
ExternalStorageServiceException(Throwable cause)350         public ExternalStorageServiceException(Throwable cause) {
351             super(cause);
352         }
353 
ExternalStorageServiceException(String message)354         public ExternalStorageServiceException(String message) {
355             super(message);
356         }
357 
ExternalStorageServiceException(String message, Throwable cause)358         public ExternalStorageServiceException(String message, Throwable cause) {
359             super(message, cause);
360         }
361     }
362 
shouldHandle(@ullable VolumeInfo vol)363     private boolean shouldHandle(@Nullable VolumeInfo vol) {
364         return mIsFuseEnabled && !mIsResetting && (vol == null || isEmulatedOrPublic(vol));
365     }
366 }
367