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