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.Nullable;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.ServiceConnection;
31 import android.os.Bundle;
32 import android.os.HandlerThread;
33 import android.os.IBinder;
34 import android.os.ParcelFileDescriptor;
35 import android.os.ParcelableException;
36 import android.os.RemoteCallback;
37 import android.os.UserHandle;
38 import android.os.UserManagerInternal;
39 import android.os.storage.StorageManagerInternal;
40 import android.os.storage.StorageVolume;
41 import android.service.storage.ExternalStorageService;
42 import android.service.storage.IExternalStorageService;
43 import android.text.TextUtils;
44 import android.util.Slog;
45 
46 import com.android.internal.annotations.GuardedBy;
47 import com.android.internal.util.Preconditions;
48 import com.android.server.LocalServices;
49 
50 import java.io.IOException;
51 import java.util.HashMap;
52 import java.util.HashSet;
53 import java.util.Map;
54 import java.util.Objects;
55 import java.util.Set;
56 import java.util.concurrent.CountDownLatch;
57 import java.util.concurrent.TimeUnit;
58 import java.util.concurrent.TimeoutException;
59 
60 /**
61  * Controls the lifecycle of the {@link ActiveConnection} to an {@link ExternalStorageService}
62  * for a user and manages storage sessions associated with mounted volumes.
63  */
64 public final class StorageUserConnection {
65     private static final String TAG = "StorageUserConnection";
66 
67     private static final int DEFAULT_REMOTE_TIMEOUT_SECONDS = 20;
68 
69     private final Object mLock = new Object();
70     private final Context mContext;
71     private final int mUserId;
72     private final StorageSessionController mSessionController;
73     private final ActiveConnection mActiveConnection = new ActiveConnection();
74     private final boolean mIsDemoUser;
75     @GuardedBy("mLock") private final Map<String, Session> mSessions = new HashMap<>();
76     @GuardedBy("mLock") @Nullable private HandlerThread mHandlerThread;
77 
StorageUserConnection(Context context, int userId, StorageSessionController controller)78     public StorageUserConnection(Context context, int userId, StorageSessionController controller) {
79         mContext = Objects.requireNonNull(context);
80         mUserId = Preconditions.checkArgumentNonnegative(userId);
81         mSessionController = controller;
82         mIsDemoUser = LocalServices.getService(UserManagerInternal.class)
83                 .getUserInfo(userId).isDemo();
84         if (mIsDemoUser) {
85             mHandlerThread = new HandlerThread("StorageUserConnectionThread-" + mUserId);
86             mHandlerThread.start();
87         }
88     }
89 
90     /**
91      * Creates and starts a storage {@link Session}.
92      *
93      * They must also be cleaned up with {@link #removeSession}.
94      *
95      * @throws IllegalArgumentException if a {@code Session} with {@code sessionId} already exists
96      */
startSession(String sessionId, ParcelFileDescriptor pfd, String upperPath, String lowerPath)97     public void startSession(String sessionId, ParcelFileDescriptor pfd, String upperPath,
98             String lowerPath) throws ExternalStorageServiceException {
99         Objects.requireNonNull(sessionId);
100         Objects.requireNonNull(pfd);
101         Objects.requireNonNull(upperPath);
102         Objects.requireNonNull(lowerPath);
103 
104         prepareRemote();
105         synchronized (mLock) {
106             Preconditions.checkArgument(!mSessions.containsKey(sessionId));
107             Session session = new Session(sessionId, upperPath, lowerPath);
108             mSessions.put(sessionId, session);
109             mActiveConnection.startSessionLocked(session, pfd);
110         }
111     }
112 
113     /**
114      * Notifies Storage Service about volume state changed.
115      *
116      * @throws ExternalStorageServiceException if failed to notify the Storage Service that
117      * {@code StorageVolume} is changed
118      */
notifyVolumeStateChanged(String sessionId, StorageVolume vol)119     public void notifyVolumeStateChanged(String sessionId, StorageVolume vol)
120             throws ExternalStorageServiceException {
121         Objects.requireNonNull(sessionId);
122         Objects.requireNonNull(vol);
123 
124         prepareRemote();
125         synchronized (mLock) {
126             mActiveConnection.notifyVolumeStateChangedLocked(sessionId, vol);
127         }
128     }
129 
130     /**
131      * Removes a session without ending it or waiting for exit.
132      *
133      * This should only be used if the session has certainly been ended because the volume was
134      * unmounted or the user running the session has been stopped. Otherwise, wait for session
135      * with {@link #waitForExit}.
136      **/
removeSession(String sessionId)137     public Session removeSession(String sessionId) {
138         synchronized (mLock) {
139             return mSessions.remove(sessionId);
140         }
141     }
142 
143     /**
144      * Removes a session and waits for exit
145      *
146      * @throws ExternalStorageServiceException if the session may not have exited
147      **/
removeSessionAndWait(String sessionId)148     public void removeSessionAndWait(String sessionId) throws ExternalStorageServiceException {
149         Session session = removeSession(sessionId);
150         if (session == null) {
151             Slog.i(TAG, "No session found for id: " + sessionId);
152             return;
153         }
154 
155         Slog.i(TAG, "Waiting for session end " + session + " ...");
156         prepareRemote();
157         synchronized (mLock) {
158             mActiveConnection.endSessionLocked(session);
159         }
160     }
161 
162     /** Restarts all available sessions for a user without blocking.
163      *
164      * Any failures will be ignored.
165      **/
resetUserSessions()166     public void resetUserSessions() {
167         synchronized (mLock) {
168             if (mSessions.isEmpty()) {
169                 // Nothing to reset if we have no sessions to restart; we typically
170                 // hit this path if the user was consciously shut down.
171                 return;
172             }
173         }
174         StorageManagerInternal sm = LocalServices.getService(StorageManagerInternal.class);
175         sm.resetUser(mUserId);
176     }
177 
178     /**
179      * Removes all sessions, without waiting.
180      */
removeAllSessions()181     public void removeAllSessions() {
182         synchronized (mLock) {
183             Slog.i(TAG, "Removing  " + mSessions.size() + " sessions for user: " + mUserId + "...");
184             mSessions.clear();
185         }
186     }
187 
188     /**
189      * Closes the connection to the {@link ExternalStorageService}. The connection will typically
190      * be restarted after close.
191      */
close()192     public void close() {
193         mActiveConnection.close();
194         if (mIsDemoUser) {
195             mHandlerThread.quit();
196         }
197     }
198 
199     /** Returns all created sessions. */
getAllSessionIds()200     public Set<String> getAllSessionIds() {
201         synchronized (mLock) {
202             return new HashSet<>(mSessions.keySet());
203         }
204     }
205 
prepareRemote()206     private void prepareRemote() throws ExternalStorageServiceException {
207         try {
208             waitForLatch(mActiveConnection.bind(), "remote_prepare_user " + mUserId);
209         } catch (IllegalStateException | TimeoutException e) {
210             throw new ExternalStorageServiceException("Failed to prepare remote", e);
211         }
212     }
213 
waitForLatch(CountDownLatch latch, String reason)214     private void waitForLatch(CountDownLatch latch, String reason) throws TimeoutException {
215         try {
216             if (!latch.await(DEFAULT_REMOTE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
217                 // TODO(b/140025078): Call ActivityManager ANR API?
218                 Slog.wtf(TAG, "Failed to bind to the ExternalStorageService for user " + mUserId);
219                 throw new TimeoutException("Latch wait for " + reason + " elapsed");
220             }
221         } catch (InterruptedException e) {
222             Thread.currentThread().interrupt();
223             throw new IllegalStateException("Latch wait for " + reason + " interrupted");
224         }
225     }
226 
227     private final class ActiveConnection implements AutoCloseable {
228         // Lifecycle connection to the external storage service, needed to unbind.
229         @GuardedBy("mLock") @Nullable private ServiceConnection mServiceConnection;
230         // True if we are connecting, either bound or binding
231         // False && mRemote != null means we are connected
232         // False && mRemote == null means we are neither connecting nor connected
233         @GuardedBy("mLock") @Nullable private boolean mIsConnecting;
234         // Binder object representing the external storage service.
235         // Non-null indicates we are connected
236         @GuardedBy("mLock") @Nullable private IExternalStorageService mRemote;
237         // Exception, if any, thrown from #startSessionLocked or #endSessionLocked
238         // Local variables cannot be referenced from a lambda expression :( so we
239         // save the exception received in the callback here. Since we guard access
240         // (and clear the exception state) with the same lock which we hold during
241         // the entire transaction, there is no risk of race.
242         @GuardedBy("mLock") @Nullable private ParcelableException mLastException;
243         // Not guarded by any lock intentionally and non final because we cannot
244         // reset latches so need to create a new one after one use
245         private CountDownLatch mLatch;
246 
247         @Override
close()248         public void close() {
249             ServiceConnection oldConnection = null;
250             synchronized (mLock) {
251                 Slog.i(TAG, "Closing connection for user " + mUserId);
252                 mIsConnecting = false;
253                 oldConnection = mServiceConnection;
254                 mServiceConnection = null;
255                 mRemote = null;
256             }
257 
258             if (oldConnection != null) {
259                 try {
260                     mContext.unbindService(oldConnection);
261                 } catch (Exception e) {
262                     // Handle IllegalArgumentException that may be thrown if the user is already
263                     // stopped when we try to unbind
264                     Slog.w(TAG, "Failed to unbind service", e);
265                 }
266             }
267         }
268 
isActiveLocked(Session session)269         public boolean isActiveLocked(Session session) {
270             if (!session.isInitialisedLocked()) {
271                 Slog.i(TAG, "Session not initialised " + session);
272                 return false;
273             }
274 
275             if (mRemote == null) {
276                 throw new IllegalStateException("Valid session with inactive connection");
277             }
278             return true;
279         }
280 
startSessionLocked(Session session, ParcelFileDescriptor fd)281         public void startSessionLocked(Session session, ParcelFileDescriptor fd)
282                 throws ExternalStorageServiceException {
283             if (!isActiveLocked(session)) {
284                 try {
285                     fd.close();
286                 } catch (IOException e) {
287                     // ignore
288                 }
289                 return;
290             }
291 
292             CountDownLatch latch = new CountDownLatch(1);
293             try {
294                 mRemote.startSession(session.sessionId,
295                         FLAG_SESSION_TYPE_FUSE | FLAG_SESSION_ATTRIBUTE_INDEXABLE,
296                         fd, session.upperPath, session.lowerPath, new RemoteCallback(result ->
297                                 setResultLocked(latch, result)));
298                 waitForLatch(latch, "start_session " + session);
299                 maybeThrowExceptionLocked();
300             } catch (Exception e) {
301                 throw new ExternalStorageServiceException("Failed to start session: " + session, e);
302             } finally {
303                 try {
304                     fd.close();
305                 } catch (IOException e) {
306                     // Ignore
307                 }
308             }
309         }
310 
endSessionLocked(Session session)311         public void endSessionLocked(Session session) throws ExternalStorageServiceException {
312             if (!isActiveLocked(session)) {
313                 // Nothing to end, not started yet
314                 return;
315             }
316 
317             CountDownLatch latch = new CountDownLatch(1);
318             try {
319                 mRemote.endSession(session.sessionId, new RemoteCallback(result ->
320                         setResultLocked(latch, result)));
321                 waitForLatch(latch, "end_session " + session);
322                 maybeThrowExceptionLocked();
323             } catch (Exception e) {
324                 throw new ExternalStorageServiceException("Failed to end session: " + session, e);
325             }
326         }
327 
notifyVolumeStateChangedLocked(String sessionId, StorageVolume vol)328         public void notifyVolumeStateChangedLocked(String sessionId, StorageVolume vol) throws
329                 ExternalStorageServiceException {
330             CountDownLatch latch = new CountDownLatch(1);
331             try {
332                 mRemote.notifyVolumeStateChanged(sessionId, vol, new RemoteCallback(
333                         result -> setResultLocked(latch, result)));
334                 waitForLatch(latch, "notify_volume_state_changed " + vol);
335                 maybeThrowExceptionLocked();
336             } catch (Exception e) {
337                 throw new ExternalStorageServiceException("Failed to notify volume state changed "
338                         + "for vol : " + vol, e);
339             }
340         }
341 
setResultLocked(CountDownLatch latch, Bundle result)342         private void setResultLocked(CountDownLatch latch, Bundle result) {
343             mLastException = result.getParcelable(EXTRA_ERROR);
344             latch.countDown();
345         }
346 
maybeThrowExceptionLocked()347         private void maybeThrowExceptionLocked() throws IOException {
348             if (mLastException != null) {
349                 ParcelableException lastException = mLastException;
350                 mLastException = null;
351                 try {
352                     lastException.maybeRethrow(IOException.class);
353                 } catch (IOException e) {
354                     throw e;
355                 }
356                 throw new RuntimeException(lastException);
357             }
358         }
359 
bind()360         public CountDownLatch bind() throws ExternalStorageServiceException {
361             ComponentName name = mSessionController.getExternalStorageServiceComponentName();
362             if (name == null) {
363                 // Not ready to bind
364                 throw new ExternalStorageServiceException(
365                         "Not ready to bind to the ExternalStorageService for user " + mUserId);
366             }
367 
368             synchronized (mLock) {
369                 if (mRemote != null || mIsConnecting) {
370                     // Connected or connecting (bound or binding)
371                     // Will wait on a latch that will countdown when we connect, unless we are
372                     // connected and the latch has already countdown, yay!
373                     return mLatch;
374                 } // else neither connected nor connecting
375 
376                 mLatch = new CountDownLatch(1);
377                 mIsConnecting = true;
378                 mServiceConnection = new ServiceConnection() {
379                     @Override
380                     public void onServiceConnected(ComponentName name, IBinder service) {
381                         Slog.i(TAG, "Service: [" + name + "] connected. User [" + mUserId + "]");
382                         handleConnection(service);
383                     }
384 
385                     @Override
386                     @MainThread
387                     public void onServiceDisconnected(ComponentName name) {
388                         // Service crashed or process was killed, #onServiceConnected will be called
389                         // Don't need to re-bind.
390                         Slog.i(TAG, "Service: [" + name + "] disconnected. User [" + mUserId + "]");
391                         handleDisconnection();
392                     }
393 
394                     @Override
395                     public void onBindingDied(ComponentName name) {
396                         // Application hosting service probably got updated
397                         // Need to re-bind.
398                         Slog.i(TAG, "Service: [" + name + "] died. User [" + mUserId + "]");
399                         handleDisconnection();
400                     }
401 
402                     @Override
403                     public void onNullBinding(ComponentName name) {
404                         Slog.wtf(TAG, "Service: [" + name + "] is null. User [" + mUserId + "]");
405                     }
406 
407                     private void handleConnection(IBinder service) {
408                         synchronized (mLock) {
409                             if (mIsConnecting) {
410                                 mRemote = IExternalStorageService.Stub.asInterface(service);
411                                 mIsConnecting = false;
412                                 mLatch.countDown();
413                                 // Separate thread so we don't block the main thead
414                                 return;
415                             }
416                         }
417                         Slog.wtf(TAG, "Connection closed to the ExternalStorageService for user "
418                                 + mUserId);
419                     }
420 
421                     private void handleDisconnection() {
422                         // Clear all sessions because we will need a new device fd since
423                         // StorageManagerService will reset the device mount state and #startSession
424                         // will be called for any required mounts.
425                         // Notify StorageManagerService so it can restart all necessary sessions
426                         close();
427                         resetUserSessions();
428                     }
429                 };
430 
431                 Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId);
432                 if (mIsDemoUser) {
433                     // Schedule on a worker thread for demo user to avoid deadlock
434                     if (mContext.bindServiceAsUser(new Intent().setComponent(name),
435                                     mServiceConnection,
436                                     Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
437                                     mHandlerThread.getThreadHandler(),
438                                     UserHandle.of(mUserId))) {
439                         Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
440                         return mLatch;
441                     } else {
442                         mIsConnecting = false;
443                         throw new ExternalStorageServiceException(
444                                 "Failed to bind to the ExternalStorageService for user " + mUserId);
445                     }
446                 } else {
447                     if (mContext.bindServiceAsUser(new Intent().setComponent(name),
448                                     mServiceConnection,
449                                     Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
450                                     UserHandle.of(mUserId))) {
451                         Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
452                         return mLatch;
453                     } else {
454                         mIsConnecting = false;
455                         throw new ExternalStorageServiceException(
456                                 "Failed to bind to the ExternalStorageService for user " + mUserId);
457                     }
458                 }
459             }
460         }
461     }
462 
463     private static final class Session {
464         public final String sessionId;
465         public final String lowerPath;
466         public final String upperPath;
467 
Session(String sessionId, String upperPath, String lowerPath)468         Session(String sessionId, String upperPath, String lowerPath) {
469             this.sessionId = sessionId;
470             this.upperPath = upperPath;
471             this.lowerPath = lowerPath;
472         }
473 
474         @Override
toString()475         public String toString() {
476             return "[SessionId: " + sessionId + ". UpperPath: " + upperPath + ". LowerPath: "
477                     + lowerPath + "]";
478         }
479 
480         @GuardedBy("mLock")
isInitialisedLocked()481         public boolean isInitialisedLocked() {
482             return !TextUtils.isEmpty(upperPath) && !TextUtils.isEmpty(lowerPath);
483         }
484     }
485 }
486