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 static android.service.storage.ExternalStorageService.EXTRA_ERROR;
20 import static android.service.storage.ExternalStorageService.FLAG_SESSION_ATTRIBUTE_INDEXABLE;
21 import static android.service.storage.ExternalStorageService.FLAG_SESSION_TYPE_FUSE;
22 
23 import static com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
24 
25 import android.annotation.MainThread;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.ServiceConnection;
32 import android.os.Bundle;
33 import android.os.HandlerThread;
34 import android.os.IBinder;
35 import android.os.ParcelFileDescriptor;
36 import android.os.ParcelableException;
37 import android.os.RemoteCallback;
38 import android.os.RemoteException;
39 import android.os.UserHandle;
40 import android.os.storage.StorageManager;
41 import android.os.storage.StorageManagerInternal;
42 import android.os.storage.StorageVolume;
43 import android.service.storage.ExternalStorageService;
44 import android.service.storage.IExternalStorageService;
45 import android.util.Slog;
46 import android.util.SparseArray;
47 
48 import com.android.internal.annotations.GuardedBy;
49 import com.android.internal.util.Preconditions;
50 import com.android.server.LocalServices;
51 
52 import java.io.IOException;
53 import java.util.ArrayList;
54 import java.util.HashMap;
55 import java.util.HashSet;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Objects;
59 import java.util.Set;
60 import java.util.concurrent.CompletableFuture;
61 import java.util.concurrent.TimeUnit;
62 import java.util.function.Consumer;
63 
64 /**
65  * Controls the lifecycle of the {@link ActiveConnection} to an {@link ExternalStorageService}
66  * for a user and manages storage sessions associated with mounted volumes.
67  */
68 public final class StorageUserConnection {
69     private static final String TAG = "StorageUserConnection";
70 
71     private static final int DEFAULT_REMOTE_TIMEOUT_SECONDS = 20;
72 
73     private final Object mSessionsLock = new Object();
74     private final Context mContext;
75     private final int mUserId;
76     private final StorageSessionController mSessionController;
77     private final StorageManagerInternal mSmInternal;
78     private final ActiveConnection mActiveConnection = new ActiveConnection();
79     @GuardedBy("mSessionsLock") private final Map<String, Session> mSessions = new HashMap<>();
80     @GuardedBy("mSessionsLock") private final SparseArray<Integer> mUidsBlockedOnIo = new SparseArray<>();
81     private final HandlerThread mHandlerThread;
82 
StorageUserConnection(Context context, int userId, StorageSessionController controller)83     public StorageUserConnection(Context context, int userId, StorageSessionController controller) {
84         mContext = Objects.requireNonNull(context);
85         mUserId = Preconditions.checkArgumentNonnegative(userId);
86         mSessionController = controller;
87         mSmInternal = LocalServices.getService(StorageManagerInternal.class);
88         mHandlerThread = new HandlerThread("StorageUserConnectionThread-" + mUserId);
89         mHandlerThread.start();
90     }
91 
92     /**
93      * Creates and starts a storage {@link Session}.
94      *
95      * They must also be cleaned up with {@link #removeSession}.
96      *
97      * @throws IllegalArgumentException if a {@code Session} with {@code sessionId} already exists
98      */
startSession(String sessionId, ParcelFileDescriptor pfd, String upperPath, String lowerPath)99     public void startSession(String sessionId, ParcelFileDescriptor pfd, String upperPath,
100             String lowerPath) throws ExternalStorageServiceException {
101         Objects.requireNonNull(sessionId);
102         Objects.requireNonNull(pfd);
103         Objects.requireNonNull(upperPath);
104         Objects.requireNonNull(lowerPath);
105 
106         Session session = new Session(sessionId, upperPath, lowerPath);
107         synchronized (mSessionsLock) {
108             Preconditions.checkArgument(!mSessions.containsKey(sessionId));
109             mSessions.put(sessionId, session);
110         }
111         mActiveConnection.startSession(session, pfd);
112     }
113 
114     /**
115      * Notifies Storage Service about volume state changed.
116      *
117      * @throws ExternalStorageServiceException if failed to notify the Storage Service that
118      * {@code StorageVolume} is changed
119      */
notifyVolumeStateChanged(String sessionId, StorageVolume vol)120     public void notifyVolumeStateChanged(String sessionId, StorageVolume vol)
121             throws ExternalStorageServiceException {
122         Objects.requireNonNull(sessionId);
123         Objects.requireNonNull(vol);
124 
125         synchronized (mSessionsLock) {
126             if (!mSessions.containsKey(sessionId)) {
127                 Slog.i(TAG, "No session found for sessionId: " + sessionId);
128                 return;
129             }
130         }
131         mActiveConnection.notifyVolumeStateChanged(sessionId, vol);
132     }
133 
134     /**
135      * Frees any cache held by ExternalStorageService.
136      *
137      * <p> Blocks until the service frees the cache or fails in doing so.
138      *
139      * @param volumeUuid uuid of the {@link StorageVolume} from which cache needs to be freed
140      * @param bytes number of bytes which need to be freed
141      * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService
142      */
freeCache(String volumeUuid, long bytes)143     public void freeCache(String volumeUuid, long bytes)
144             throws ExternalStorageServiceException {
145         synchronized (mSessionsLock) {
146             for (String sessionId : mSessions.keySet()) {
147                 mActiveConnection.freeCache(sessionId, volumeUuid, bytes);
148             }
149         }
150     }
151 
152     /**
153      * Called when {@code packageName} is about to ANR
154      *
155      * @return ANR dialog delay in milliseconds
156      */
notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)157     public void notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)
158             throws ExternalStorageServiceException {
159         List<String> primarySessionIds = mSmInternal.getPrimaryVolumeIds();
160         synchronized (mSessionsLock) {
161             for (String sessionId : mSessions.keySet()) {
162                 if (primarySessionIds.contains(sessionId)) {
163                     mActiveConnection.notifyAnrDelayStarted(packageName, uid, tid, reason);
164                     return;
165                 }
166             }
167         }
168     }
169 
170     /**
171      * Removes a session without ending it or waiting for exit.
172      *
173      * This should only be used if the session has certainly been ended because the volume was
174      * unmounted or the user running the session has been stopped. Otherwise, wait for session
175      * with {@link #waitForExit}.
176      **/
removeSession(String sessionId)177     public Session removeSession(String sessionId) {
178         synchronized (mSessionsLock) {
179             mUidsBlockedOnIo.clear();
180             return mSessions.remove(sessionId);
181         }
182     }
183 
184     /**
185      * Removes a session and waits for exit
186      *
187      * @throws ExternalStorageServiceException if the session may not have exited
188      **/
removeSessionAndWait(String sessionId)189     public void removeSessionAndWait(String sessionId) throws ExternalStorageServiceException {
190         Session session = removeSession(sessionId);
191         if (session == null) {
192             Slog.i(TAG, "No session found for id: " + sessionId);
193             return;
194         }
195 
196         Slog.i(TAG, "Waiting for session end " + session + " ...");
197         mActiveConnection.endSession(session);
198     }
199 
200     /** Restarts all available sessions for a user without blocking.
201      *
202      * Any failures will be ignored.
203      **/
resetUserSessions()204     public void resetUserSessions() {
205         synchronized (mSessionsLock) {
206             if (mSessions.isEmpty()) {
207                 // Nothing to reset if we have no sessions to restart; we typically
208                 // hit this path if the user was consciously shut down.
209                 return;
210             }
211         }
212         mSmInternal.resetUser(mUserId);
213     }
214 
215     /**
216      * Removes all sessions, without waiting.
217      */
removeAllSessions()218     public void removeAllSessions() {
219         synchronized (mSessionsLock) {
220             Slog.i(TAG, "Removing  " + mSessions.size() + " sessions for user: " + mUserId + "...");
221             mSessions.clear();
222         }
223     }
224 
225     /**
226      * Closes the connection to the {@link ExternalStorageService}. The connection will typically
227      * be restarted after close.
228      */
close()229     public void close() {
230         mActiveConnection.close();
231         mHandlerThread.quit();
232     }
233 
234     /** Returns all created sessions. */
getAllSessionIds()235     public Set<String> getAllSessionIds() {
236         synchronized (mSessionsLock) {
237             return new HashSet<>(mSessions.keySet());
238         }
239     }
240 
241     /**
242      * Notify the controller that an app with {@code uid} and {@code tid} is blocked on an IO
243      * request on {@code volumeUuid} for {@code reason}.
244      *
245      * This blocked state can be queried with {@link #isAppIoBlocked}
246      *
247      * @hide
248      */
notifyAppIoBlocked(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)249     public void notifyAppIoBlocked(String volumeUuid, int uid, int tid,
250             @StorageManager.AppIoBlockedReason int reason) {
251         synchronized (mSessionsLock) {
252             int ioBlockedCounter = mUidsBlockedOnIo.get(uid, 0);
253             mUidsBlockedOnIo.put(uid, ++ioBlockedCounter);
254         }
255     }
256 
257     /**
258      * Notify the connection that an app with {@code uid} and {@code tid} has resmed a previously
259      * blocked IO request on {@code volumeUuid} for {@code reason}.
260      *
261      * All app IO will be automatically marked as unblocked if {@code volumeUuid} is unmounted.
262      */
notifyAppIoResumed(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)263     public void notifyAppIoResumed(String volumeUuid, int uid, int tid,
264             @StorageManager.AppIoBlockedReason int reason) {
265         synchronized (mSessionsLock) {
266             int ioBlockedCounter = mUidsBlockedOnIo.get(uid, 0);
267             if (ioBlockedCounter == 0) {
268                 Slog.w(TAG, "Unexpected app IO resumption for uid: " + uid);
269             }
270 
271             if (ioBlockedCounter <= 1) {
272                 mUidsBlockedOnIo.remove(uid);
273             } else {
274                 mUidsBlockedOnIo.put(uid, --ioBlockedCounter);
275             }
276         }
277     }
278 
279     /** Returns {@code true} if {@code uid} is blocked on IO, {@code false} otherwise */
isAppIoBlocked(int uid)280     public boolean isAppIoBlocked(int uid) {
281         synchronized (mSessionsLock) {
282             return mUidsBlockedOnIo.contains(uid);
283         }
284     }
285 
286     @FunctionalInterface
287     interface AsyncStorageServiceCall {
run(@onNull IExternalStorageService service, RemoteCallback callback)288         void run(@NonNull IExternalStorageService service, RemoteCallback callback) throws
289                 RemoteException;
290     }
291 
292     private final class ActiveConnection implements AutoCloseable {
293         private final Object mLock = new Object();
294 
295         // Lifecycle connection to the external storage service, needed to unbind.
296         @GuardedBy("mLock") @Nullable private ServiceConnection mServiceConnection;
297 
298         // A future that holds the remote interface
299         @GuardedBy("mLock")
300         @Nullable private CompletableFuture<IExternalStorageService> mRemoteFuture;
301 
302         // A list of outstanding futures for async calls, for which we are still waiting
303         // for a callback. Used to unblock waiters if the service dies.
304         @GuardedBy("mLock")
305         private final ArrayList<CompletableFuture<Void>> mOutstandingOps = new ArrayList<>();
306 
307         @Override
close()308         public void close() {
309             ServiceConnection oldConnection = null;
310             synchronized (mLock) {
311                 Slog.i(TAG, "Closing connection for user " + mUserId);
312                 oldConnection = mServiceConnection;
313                 mServiceConnection = null;
314                 if (mRemoteFuture != null) {
315                     // Let folks who are waiting for the connection know it ain't gonna happen
316                     mRemoteFuture.cancel(true);
317                     mRemoteFuture = null;
318                 }
319                 // Let folks waiting for callbacks from the remote know it ain't gonna happen
320                 for (CompletableFuture<Void> op : mOutstandingOps) {
321                     op.cancel(true);
322                 }
323                 mOutstandingOps.clear();
324             }
325 
326             if (oldConnection != null) {
327                 try {
328                     mContext.unbindService(oldConnection);
329                 } catch (Exception e) {
330                     // Handle IllegalArgumentException that may be thrown if the user is already
331                     // stopped when we try to unbind
332                     Slog.w(TAG, "Failed to unbind service", e);
333                 }
334             }
335         }
336 
asyncBestEffort(Consumer<IExternalStorageService> consumer)337         private void asyncBestEffort(Consumer<IExternalStorageService> consumer) {
338             synchronized (mLock) {
339                 if (mRemoteFuture == null) {
340                     Slog.w(TAG, "Dropping async request service is not bound");
341                     return;
342                 }
343 
344                 IExternalStorageService service = mRemoteFuture.getNow(null);
345                 if (service == null) {
346                     Slog.w(TAG, "Dropping async request service is not connected");
347                     return;
348                 }
349 
350                 consumer.accept(service);
351             }
352         }
353 
waitForAsyncVoid(AsyncStorageServiceCall asyncCall)354         private void waitForAsyncVoid(AsyncStorageServiceCall asyncCall) throws Exception {
355             CompletableFuture<Void> opFuture = new CompletableFuture<>();
356             RemoteCallback callback = new RemoteCallback(result -> setResult(result, opFuture));
357 
358             waitForAsync(asyncCall, callback, opFuture, mOutstandingOps,
359                     DEFAULT_REMOTE_TIMEOUT_SECONDS);
360         }
361 
waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback, CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps, long timeoutSeconds)362         private <T> T waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback,
363                 CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps,
364                 long timeoutSeconds) throws Exception {
365             CompletableFuture<IExternalStorageService> serviceFuture = connectIfNeeded();
366 
367             try {
368                 synchronized (mLock) {
369                     outstandingOps.add(opFuture);
370                 }
371                 return serviceFuture.thenCompose(service -> {
372                     try {
373                         asyncCall.run(service, callback);
374                     } catch (RemoteException e) {
375                         opFuture.completeExceptionally(e);
376                     }
377 
378                     return opFuture;
379                 }).get(timeoutSeconds, TimeUnit.SECONDS);
380             } finally {
381                 synchronized (mLock) {
382                     outstandingOps.remove(opFuture);
383                 }
384             }
385         }
386 
startSession(Session session, ParcelFileDescriptor fd)387         public void startSession(Session session, ParcelFileDescriptor fd)
388                 throws ExternalStorageServiceException {
389             try {
390                 waitForAsyncVoid((service, callback) -> service.startSession(session.sessionId,
391                         FLAG_SESSION_TYPE_FUSE | FLAG_SESSION_ATTRIBUTE_INDEXABLE,
392                                 fd, session.upperPath, session.lowerPath, callback));
393             } catch (Exception e) {
394                 throw new ExternalStorageServiceException("Failed to start session: " + session, e);
395             } finally {
396                 try {
397                     fd.close();
398                 } catch (IOException e) {
399                     // Ignore
400                 }
401             }
402         }
403 
endSession(Session session)404         public void endSession(Session session) throws ExternalStorageServiceException {
405             try {
406                 waitForAsyncVoid((service, callback) ->
407                         service.endSession(session.sessionId, callback));
408             } catch (Exception e) {
409                 throw new ExternalStorageServiceException("Failed to end session: " + session, e);
410             }
411         }
412 
413 
notifyVolumeStateChanged(String sessionId, StorageVolume vol)414         public void notifyVolumeStateChanged(String sessionId, StorageVolume vol) throws
415                 ExternalStorageServiceException {
416             try {
417                 waitForAsyncVoid((service, callback) ->
418                         service.notifyVolumeStateChanged(sessionId, vol, callback));
419             } catch (Exception e) {
420                 throw new ExternalStorageServiceException("Failed to notify volume state changed "
421                         + "for vol : " + vol, e);
422             }
423         }
424 
freeCache(String sessionId, String volumeUuid, long bytes)425         public void freeCache(String sessionId, String volumeUuid, long bytes)
426                 throws ExternalStorageServiceException {
427             try {
428                 waitForAsyncVoid((service, callback) ->
429                         service.freeCache(sessionId, volumeUuid, bytes, callback));
430             } catch (Exception e) {
431                 throw new ExternalStorageServiceException("Failed to free " + bytes
432                         + " bytes for volumeUuid : " + volumeUuid, e);
433             }
434         }
435 
notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason)436         public void notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason)
437                 throws ExternalStorageServiceException {
438             asyncBestEffort(service -> {
439                 try {
440                     service.notifyAnrDelayStarted(packgeName, uid, tid, reason);
441                 } catch (RemoteException e) {
442                     Slog.w(TAG, "Failed to notify ANR delay started", e);
443                 }
444             });
445         }
446 
setResult(Bundle result, CompletableFuture<Void> future)447         private void setResult(Bundle result, CompletableFuture<Void> future) {
448             ParcelableException ex = result.getParcelable(EXTRA_ERROR, android.os.ParcelableException.class);
449             if (ex != null) {
450                 future.completeExceptionally(ex);
451             } else {
452                 future.complete(null);
453             }
454         }
455 
connectIfNeeded()456         private CompletableFuture<IExternalStorageService> connectIfNeeded() throws
457                 ExternalStorageServiceException {
458             ComponentName name = mSessionController.getExternalStorageServiceComponentName();
459             if (name == null) {
460                 // Not ready to bind
461                 throw new ExternalStorageServiceException(
462                         "Not ready to bind to the ExternalStorageService for user " + mUserId);
463             }
464             synchronized (mLock) {
465                 if (mRemoteFuture != null) {
466                     return mRemoteFuture;
467                 }
468                 CompletableFuture<IExternalStorageService> future = new CompletableFuture<>();
469                 mServiceConnection = new ServiceConnection() {
470                     @Override
471                     public void onServiceConnected(ComponentName name, IBinder service) {
472                         Slog.i(TAG, "Service: [" + name + "] connected. User [" + mUserId + "]");
473                         handleConnection(service);
474                     }
475 
476                     @Override
477                     @MainThread
478                     public void onServiceDisconnected(ComponentName name) {
479                         // Service crashed or process was killed, #onServiceConnected will be called
480                         // Don't need to re-bind.
481                         Slog.i(TAG, "Service: [" + name + "] disconnected. User [" + mUserId + "]");
482                         handleDisconnection();
483                     }
484 
485                     @Override
486                     public void onBindingDied(ComponentName name) {
487                         // Application hosting service probably got updated
488                         // Need to re-bind.
489                         Slog.i(TAG, "Service: [" + name + "] died. User [" + mUserId + "]");
490                         handleDisconnection();
491                     }
492 
493                     @Override
494                     public void onNullBinding(ComponentName name) {
495                         Slog.wtf(TAG, "Service: [" + name + "] is null. User [" + mUserId + "]");
496                     }
497 
498                     private void handleConnection(IBinder service) {
499                         synchronized (mLock) {
500                             future.complete(
501                                     IExternalStorageService.Stub.asInterface(service));
502                         }
503                     }
504 
505                     private void handleDisconnection() {
506                         // Clear all sessions because we will need a new device fd since
507                         // StorageManagerService will reset the device mount state and #startSession
508                         // will be called for any required mounts.
509                         // Notify StorageManagerService so it can restart all necessary sessions
510                         close();
511                         resetUserSessions();
512                     }
513                 };
514 
515                 Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId);
516                 // Schedule on a worker thread, because the system server main thread can be
517                 // very busy early in boot.
518                 if (mContext.bindServiceAsUser(new Intent().setComponent(name),
519                                 mServiceConnection,
520                                 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
521                                 mHandlerThread.getThreadHandler(),
522                                 UserHandle.of(mUserId))) {
523                     Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
524                     mRemoteFuture = future;
525                     return future;
526                 } else {
527                     throw new ExternalStorageServiceException(
528                             "Failed to bind to the ExternalStorageService for user " + mUserId);
529                 }
530             }
531         }
532     }
533 
534     private static final class Session {
535         public final String sessionId;
536         public final String lowerPath;
537         public final String upperPath;
538 
539         Session(String sessionId, String upperPath, String lowerPath) {
540             this.sessionId = sessionId;
541             this.upperPath = upperPath;
542             this.lowerPath = lowerPath;
543         }
544 
545         @Override
546         public String toString() {
547             return "[SessionId: " + sessionId + ". UpperPath: " + upperPath + ". LowerPath: "
548                     + lowerPath + "]";
549         }
550     }
551 }
552