• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
354 
waitForAsyncVoid(AsyncStorageServiceCall asyncCall)355         private void waitForAsyncVoid(AsyncStorageServiceCall asyncCall) throws Exception {
356             waitForAsyncVoid(asyncCall, /*bindIfNotConnected*/ true,
357                     DEFAULT_REMOTE_TIMEOUT_SECONDS);
358         }
359 
waitForAsyncVoid(AsyncStorageServiceCall asyncCall, boolean bindIfNotConnected, int timeoutSeconds)360         private void waitForAsyncVoid(AsyncStorageServiceCall asyncCall,
361                 boolean bindIfNotConnected, int timeoutSeconds) throws Exception {
362             CompletableFuture<Void> opFuture = new CompletableFuture<>();
363             RemoteCallback callback = new RemoteCallback(result -> setResult(result, opFuture));
364 
365             waitForAsync(asyncCall, callback, opFuture, mOutstandingOps, bindIfNotConnected,
366                     timeoutSeconds);
367         }
368 
waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback, CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps, boolean bindIfNotConnected, long timeoutSeconds)369         private <T> T waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback,
370                 CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps,
371                 boolean bindIfNotConnected, long timeoutSeconds) throws Exception {
372 
373             CompletableFuture<IExternalStorageService> serviceFuture;
374             if (bindIfNotConnected) {
375                 serviceFuture = connectIfNeeded();
376             } else {
377                 synchronized (mLock) {
378                     if (mRemoteFuture == null || mRemoteFuture.getNow(null) == null) {
379                         Slog.w(TAG, "Dropping async request as service is not connected"
380                                 + "and request doesn't require connecting");
381                         return null;
382                     }
383                     serviceFuture = mRemoteFuture;
384                 }
385             }
386 
387             try {
388                 synchronized (mLock) {
389                     outstandingOps.add(opFuture);
390                 }
391                 return serviceFuture.thenCompose(service -> {
392                     try {
393                         asyncCall.run(service, callback);
394                     } catch (RemoteException e) {
395                         opFuture.completeExceptionally(e);
396                     }
397 
398                     return opFuture;
399                 }).get(timeoutSeconds, TimeUnit.SECONDS);
400             } finally {
401                 synchronized (mLock) {
402                     outstandingOps.remove(opFuture);
403                 }
404             }
405         }
406 
startSession(Session session, ParcelFileDescriptor fd)407         public void startSession(Session session, ParcelFileDescriptor fd)
408                 throws ExternalStorageServiceException {
409             try {
410                 waitForAsyncVoid((service, callback) -> service.startSession(session.sessionId,
411                         FLAG_SESSION_TYPE_FUSE | FLAG_SESSION_ATTRIBUTE_INDEXABLE,
412                                 fd, session.upperPath, session.lowerPath, callback));
413             } catch (Exception e) {
414                 throw new ExternalStorageServiceException("Failed to start session: " + session, e);
415             } finally {
416                 try {
417                     fd.close();
418                 } catch (IOException e) {
419                     // Ignore
420                 }
421             }
422         }
423 
endSession(Session session)424         public void endSession(Session session) throws ExternalStorageServiceException {
425             try {
426                 waitForAsyncVoid((service, callback) ->
427                         service.endSession(session.sessionId, callback),
428                         // endSession shouldn't be trying to bind to remote service if the service
429                         // isn't connected already as this means that no previous mounting has been
430                         // completed.
431                         /*bindIfNotConnected*/ false, /*timeoutSeconds*/ 10);
432             } catch (Exception e) {
433                 throw new ExternalStorageServiceException("Failed to end session: " + session, e);
434             }
435         }
436 
437 
notifyVolumeStateChanged(String sessionId, StorageVolume vol)438         public void notifyVolumeStateChanged(String sessionId, StorageVolume vol) throws
439                 ExternalStorageServiceException {
440             try {
441                 waitForAsyncVoid((service, callback) ->
442                         service.notifyVolumeStateChanged(sessionId, vol, callback),
443                         // notifyVolumeStateChanged shouldn't be trying to bind to remote service
444                         // if the service isn't connected already as this means that
445                         // no previous mounting has been completed
446                         /*bindIfNotConnected*/ false, /*timeoutSeconds*/ 10);
447             } catch (Exception e) {
448                 throw new ExternalStorageServiceException("Failed to notify volume state changed "
449                         + "for vol : " + vol, e);
450             }
451         }
452 
freeCache(String sessionId, String volumeUuid, long bytes)453         public void freeCache(String sessionId, String volumeUuid, long bytes)
454                 throws ExternalStorageServiceException {
455             try {
456                 waitForAsyncVoid((service, callback) ->
457                         service.freeCache(sessionId, volumeUuid, bytes, callback));
458             } catch (Exception e) {
459                 throw new ExternalStorageServiceException("Failed to free " + bytes
460                         + " bytes for volumeUuid : " + volumeUuid, e);
461             }
462         }
463 
notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason)464         public void notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason)
465                 throws ExternalStorageServiceException {
466             asyncBestEffort(service -> {
467                 try {
468                     service.notifyAnrDelayStarted(packgeName, uid, tid, reason);
469                 } catch (RemoteException e) {
470                     Slog.w(TAG, "Failed to notify ANR delay started", e);
471                 }
472             });
473         }
474 
setResult(Bundle result, CompletableFuture<Void> future)475         private void setResult(Bundle result, CompletableFuture<Void> future) {
476             ParcelableException ex = result.getParcelable(EXTRA_ERROR, android.os.ParcelableException.class);
477             if (ex != null) {
478                 future.completeExceptionally(ex);
479             } else {
480                 future.complete(null);
481             }
482         }
483 
connectIfNeeded()484         private CompletableFuture<IExternalStorageService> connectIfNeeded() throws
485                 ExternalStorageServiceException {
486             ComponentName name = mSessionController.getExternalStorageServiceComponentName();
487             if (name == null) {
488                 // Not ready to bind
489                 throw new ExternalStorageServiceException(
490                         "Not ready to bind to the ExternalStorageService for user " + mUserId);
491             }
492             synchronized (mLock) {
493                 if (mRemoteFuture != null) {
494                     return mRemoteFuture;
495                 }
496                 CompletableFuture<IExternalStorageService> future = new CompletableFuture<>();
497                 mServiceConnection = new ServiceConnection() {
498                     @Override
499                     public void onServiceConnected(ComponentName name, IBinder service) {
500                         Slog.i(TAG, "Service: [" + name + "] connected. User [" + mUserId + "]");
501                         handleConnection(service);
502                     }
503 
504                     @Override
505                     @MainThread
506                     public void onServiceDisconnected(ComponentName name) {
507                         // Service crashed or process was killed, #onServiceConnected will be called
508                         // Don't need to re-bind.
509                         Slog.i(TAG, "Service: [" + name + "] disconnected. User [" + mUserId + "]");
510                         handleDisconnection();
511                     }
512 
513                     @Override
514                     public void onBindingDied(ComponentName name) {
515                         // Application hosting service probably got updated
516                         // Need to re-bind.
517                         Slog.i(TAG, "Service: [" + name + "] died. User [" + mUserId + "]");
518                         handleDisconnection();
519                     }
520 
521                     @Override
522                     public void onNullBinding(ComponentName name) {
523                         Slog.wtf(TAG, "Service: [" + name + "] is null. User [" + mUserId + "]");
524                     }
525 
526                     private void handleConnection(IBinder service) {
527                         synchronized (mLock) {
528                             future.complete(
529                                     IExternalStorageService.Stub.asInterface(service));
530                         }
531                     }
532 
533                     private void handleDisconnection() {
534                         // Clear all sessions because we will need a new device fd since
535                         // StorageManagerService will reset the device mount state and #startSession
536                         // will be called for any required mounts.
537                         // Notify StorageManagerService so it can restart all necessary sessions
538                         close();
539                         resetUserSessions();
540                     }
541                 };
542 
543                 Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId);
544                 // Schedule on a worker thread, because the system server main thread can be
545                 // very busy early in boot.
546                 if (mContext.bindServiceAsUser(new Intent().setComponent(name),
547                                 mServiceConnection,
548                                 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
549                                 mHandlerThread.getThreadHandler(),
550                                 UserHandle.of(mUserId))) {
551                     Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
552                     mRemoteFuture = future;
553                     return future;
554                 } else {
555                     throw new ExternalStorageServiceException(
556                             "Failed to bind to the ExternalStorageService for user " + mUserId);
557                 }
558             }
559         }
560     }
561 
562     private static final class Session {
563         public final String sessionId;
564         public final String lowerPath;
565         public final String upperPath;
566 
567         Session(String sessionId, String upperPath, String lowerPath) {
568             this.sessionId = sessionId;
569             this.upperPath = upperPath;
570             this.lowerPath = lowerPath;
571         }
572 
573         @Override
574         public String toString() {
575             return "[SessionId: " + sessionId + ". UpperPath: " + upperPath + ". LowerPath: "
576                     + lowerPath + "]";
577         }
578     }
579 }
580