1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.storage; 18 19 import android.Manifest; 20 import android.annotation.Nullable; 21 import android.app.ActivityManager; 22 import android.app.ApplicationExitInfo; 23 import android.app.IActivityManager; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.content.pm.ProviderInfo; 29 import android.content.pm.ResolveInfo; 30 import android.content.pm.ServiceInfo; 31 import android.content.pm.UserInfo; 32 import android.os.Binder; 33 import android.os.IVold; 34 import android.os.ParcelFileDescriptor; 35 import android.os.RemoteException; 36 import android.os.ServiceSpecificException; 37 import android.os.UserHandle; 38 import android.os.UserManager; 39 import android.os.storage.StorageManager; 40 import android.os.storage.StorageVolume; 41 import android.os.storage.VolumeInfo; 42 import android.provider.MediaStore; 43 import android.service.storage.ExternalStorageService; 44 import android.util.Slog; 45 import android.util.SparseArray; 46 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.server.storage.ImmutableVolumeInfo; 49 50 import java.util.Objects; 51 52 /** 53 * Controls storage sessions for users initiated by the {@link StorageManagerService}. 54 * Each user on the device will be represented by a {@link StorageUserConnection}. 55 */ 56 public final class StorageSessionController { 57 private static final String TAG = "StorageSessionController"; 58 59 private final Object mLock = new Object(); 60 private final Context mContext; 61 private final UserManager mUserManager; 62 @GuardedBy("mLock") 63 private final SparseArray<StorageUserConnection> mConnections = new SparseArray<>(); 64 65 private volatile ComponentName mExternalStorageServiceComponent; 66 private volatile String mExternalStorageServicePackageName; 67 private volatile int mExternalStorageServiceAppId; 68 private volatile boolean mIsResetting; 69 StorageSessionController(Context context)70 public StorageSessionController(Context context) { 71 mContext = Objects.requireNonNull(context); 72 mUserManager = mContext.getSystemService(UserManager.class); 73 } 74 75 /** 76 * Returns userId for the volume to be used in the StorageUserConnection. 77 * If the user is a clone profile, it will use the same connection 78 * as the parent user, and hence this method returns the parent's userId. Else, it returns the 79 * volume's mountUserId 80 * @param vol for which the storage session has to be started 81 * @return userId for connection for this volume 82 */ getConnectionUserIdForVolume(ImmutableVolumeInfo vol)83 public int getConnectionUserIdForVolume(ImmutableVolumeInfo vol) { 84 final Context volumeUserContext = mContext.createContextAsUser( 85 UserHandle.of(vol.getMountUserId()), 0); 86 boolean isMediaSharedWithParent = volumeUserContext.getSystemService( 87 UserManager.class).isMediaSharedWithParent(); 88 89 UserInfo userInfo = mUserManager.getUserInfo(vol.getMountUserId()); 90 if (userInfo != null && isMediaSharedWithParent) { 91 // Clones use the same connection as their parent 92 return userInfo.profileGroupId; 93 } else { 94 return vol.getMountUserId(); 95 } 96 } 97 98 /** 99 * Creates and starts a storage session associated with {@code deviceFd} for {@code vol}. 100 * Sessions can be started with {@link #onVolumeReady} and removed with {@link #onVolumeUnmount} 101 * or {@link #onVolumeRemove}. 102 * 103 * Throws an {@link IllegalStateException} if a session for {@code vol} has already been created 104 * 105 * Does nothing if {@link #shouldHandle} is {@code false} 106 * 107 * Blocks until the session is started or fails 108 * 109 * @throws ExternalStorageServiceException if the session fails to start 110 * @throws IllegalStateException if a session has already been created for {@code vol} 111 */ onVolumeMount(ParcelFileDescriptor deviceFd, ImmutableVolumeInfo vol)112 public void onVolumeMount(ParcelFileDescriptor deviceFd, ImmutableVolumeInfo vol) 113 throws ExternalStorageServiceException { 114 if (!shouldHandle(vol)) { 115 return; 116 } 117 118 Slog.i(TAG, "On volume mount " + vol); 119 120 String sessionId = vol.getId(); 121 int userId = getConnectionUserIdForVolume(vol); 122 123 StorageUserConnection connection = null; 124 synchronized (mLock) { 125 connection = mConnections.get(userId); 126 if (connection == null) { 127 Slog.i(TAG, "Creating connection for user: " + userId); 128 connection = new StorageUserConnection(mContext, userId, this); 129 mConnections.put(userId, connection); 130 } 131 } 132 Slog.i(TAG, "Creating and starting session with id: " + sessionId); 133 connection.startSession(sessionId, deviceFd, vol.getPath().getPath(), 134 vol.getInternalPath().getPath()); 135 } 136 137 /** 138 * Notifies the Storage Service that volume state for {@code vol} is changed. 139 * A session may already be created for this volume if it is mounted before or the volume state 140 * has changed to mounted. 141 * 142 * Does nothing if {@link #shouldHandle} is {@code false} 143 * 144 * Blocks until the Storage Service processes/scans the volume or fails in doing so. 145 * 146 * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService 147 */ notifyVolumeStateChanged(ImmutableVolumeInfo vol)148 public void notifyVolumeStateChanged(ImmutableVolumeInfo vol) 149 throws ExternalStorageServiceException { 150 if (!shouldHandle(vol)) { 151 return; 152 } 153 String sessionId = vol.getId(); 154 int connectionUserId = getConnectionUserIdForVolume(vol); 155 156 StorageUserConnection connection = null; 157 synchronized (mLock) { 158 connection = mConnections.get(connectionUserId); 159 } 160 161 if (connection != null) { 162 Slog.i(TAG, "Notifying volume state changed for session with id: " + sessionId); 163 connection.notifyVolumeStateChanged(sessionId, 164 vol.buildStorageVolume(mContext, vol.getMountUserId(), false)); 165 } else { 166 Slog.w(TAG, "No available storage user connection for userId : " 167 + connectionUserId); 168 } 169 } 170 171 /** 172 * Frees any cache held by ExternalStorageService. 173 * 174 * <p> Blocks until the service frees the cache or fails in doing so. 175 * 176 * @param volumeUuid uuid of the {@link StorageVolume} from which cache needs to be freed 177 * @param bytes number of bytes which need to be freed 178 * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService 179 */ freeCache(String volumeUuid, long bytes)180 public void freeCache(String volumeUuid, long bytes) 181 throws ExternalStorageServiceException { 182 synchronized (mLock) { 183 int size = mConnections.size(); 184 for (int i = 0; i < size; i++) { 185 int key = mConnections.keyAt(i); 186 StorageUserConnection connection = mConnections.get(key); 187 if (connection != null) { 188 connection.freeCache(volumeUuid, bytes); 189 } 190 } 191 } 192 } 193 194 /** 195 * Called when {@code packageName} is about to ANR 196 * 197 * @return ANR dialog delay in milliseconds 198 */ notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)199 public void notifyAnrDelayStarted(String packageName, int uid, int tid, int reason) 200 throws ExternalStorageServiceException { 201 final int userId = UserHandle.getUserId(uid); 202 final StorageUserConnection connection; 203 synchronized (mLock) { 204 connection = mConnections.get(userId); 205 } 206 207 if (connection != null) { 208 connection.notifyAnrDelayStarted(packageName, uid, tid, reason); 209 } 210 } 211 212 /** 213 * Removes and returns the {@link StorageUserConnection} for {@code vol}. 214 * 215 * Does nothing if {@link #shouldHandle} is {@code false} 216 * 217 * @return the connection that was removed or {@code null} if nothing was removed 218 */ 219 @Nullable onVolumeRemove(ImmutableVolumeInfo vol)220 public StorageUserConnection onVolumeRemove(ImmutableVolumeInfo vol) { 221 if (!shouldHandle(vol)) { 222 return null; 223 } 224 225 Slog.i(TAG, "On volume remove " + vol); 226 String sessionId = vol.getId(); 227 int userId = getConnectionUserIdForVolume(vol); 228 229 StorageUserConnection connection = null; 230 synchronized (mLock) { 231 connection = mConnections.get(userId); 232 } 233 234 if (connection != null) { 235 Slog.i(TAG, "Removed session for vol with id: " + sessionId); 236 connection.removeSession(sessionId); 237 return connection; 238 } else { 239 Slog.w(TAG, "Session already removed for vol with id: " + sessionId); 240 return null; 241 } 242 } 243 244 245 /** 246 * Removes a storage session for {@code vol} and waits for exit. 247 * 248 * Does nothing if {@link #shouldHandle} is {@code false} 249 * 250 * Any errors are ignored 251 * 252 * Call {@link #onVolumeRemove} to remove the connection without waiting for exit 253 */ onVolumeUnmount(ImmutableVolumeInfo vol)254 public void onVolumeUnmount(ImmutableVolumeInfo vol) { 255 String sessionId = vol.getId(); 256 final long token = Binder.clearCallingIdentity(); 257 try { 258 StorageUserConnection connection = onVolumeRemove(vol); 259 Slog.i(TAG, "On volume unmount " + vol); 260 if (connection != null) { 261 connection.removeSessionAndWait(sessionId); 262 } 263 } catch (ExternalStorageServiceException e) { 264 Slog.e(TAG, "Failed to end session for vol with id: " + sessionId, e); 265 } finally { 266 Binder.restoreCallingIdentity(token); 267 } 268 } 269 270 /** 271 * Makes sure we initialize the ExternalStorageService component. 272 */ onUnlockUser(int userId)273 public void onUnlockUser(int userId) throws ExternalStorageServiceException { 274 Slog.i(TAG, "On user unlock " + userId); 275 if (userId == 0) { 276 initExternalStorageServiceComponent(); 277 } 278 } 279 280 /** 281 * Called when a user is in the process is being stopped. 282 * 283 * Does nothing if {@link #shouldHandle} is {@code false} 284 * 285 * This call removes all sessions for the user that is being stopped; 286 * this will make sure that we don't rebind to the service needlessly. 287 */ onUserStopping(int userId)288 public void onUserStopping(int userId) { 289 if (!shouldHandle(null)) { 290 return; 291 } 292 StorageUserConnection connection = null; 293 synchronized (mLock) { 294 connection = mConnections.get(userId); 295 } 296 297 if (connection != null) { 298 Slog.i(TAG, "Removing all sessions for user: " + userId); 299 connection.removeAllSessions(); 300 } else { 301 Slog.w(TAG, "No connection found for user: " + userId); 302 } 303 } 304 305 /** 306 * Resets all sessions for all users and waits for exit. This may kill the 307 * {@link ExternalStorageservice} for a user if necessary to ensure all state has been reset. 308 * 309 * Does nothing if {@link #shouldHandle} is {@code false} 310 **/ onReset(IVold vold, Runnable resetHandlerRunnable)311 public void onReset(IVold vold, Runnable resetHandlerRunnable) { 312 if (!shouldHandle(null)) { 313 return; 314 } 315 316 SparseArray<StorageUserConnection> connections = new SparseArray(); 317 synchronized (mLock) { 318 mIsResetting = true; 319 Slog.i(TAG, "Started resetting external storage service..."); 320 for (int i = 0; i < mConnections.size(); i++) { 321 connections.put(mConnections.keyAt(i), mConnections.valueAt(i)); 322 } 323 } 324 325 for (int i = 0; i < connections.size(); i++) { 326 StorageUserConnection connection = connections.valueAt(i); 327 for (String sessionId : connection.getAllSessionIds()) { 328 try { 329 Slog.i(TAG, "Unmounting " + sessionId); 330 vold.unmount(sessionId); 331 Slog.i(TAG, "Unmounted " + sessionId); 332 } catch (ServiceSpecificException | RemoteException e) { 333 // TODO(b/140025078): Hard reset vold? 334 Slog.e(TAG, "Failed to unmount volume: " + sessionId, e); 335 } 336 337 try { 338 Slog.i(TAG, "Exiting " + sessionId); 339 connection.removeSessionAndWait(sessionId); 340 Slog.i(TAG, "Exited " + sessionId); 341 } catch (IllegalStateException | ExternalStorageServiceException e) { 342 Slog.e(TAG, "Failed to exit session: " + sessionId 343 + ". Killing MediaProvider...", e); 344 // If we failed to confirm the session exited, it is risky to proceed 345 // We kill the ExternalStorageService as a last resort 346 killExternalStorageService(connections.keyAt(i)); 347 break; 348 } 349 } 350 connection.close(); 351 } 352 353 resetHandlerRunnable.run(); 354 synchronized (mLock) { 355 mConnections.clear(); 356 mIsResetting = false; 357 Slog.i(TAG, "Finished resetting external storage service"); 358 } 359 } 360 initExternalStorageServiceComponent()361 private void initExternalStorageServiceComponent() throws ExternalStorageServiceException { 362 Slog.i(TAG, "Initialialising..."); 363 ProviderInfo provider = mContext.getPackageManager().resolveContentProvider( 364 MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE 365 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 366 | PackageManager.MATCH_SYSTEM_ONLY); 367 if (provider == null) { 368 throw new ExternalStorageServiceException("No valid MediaStore provider found"); 369 } 370 371 mExternalStorageServicePackageName = provider.applicationInfo.packageName; 372 mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid); 373 374 ServiceInfo serviceInfo = resolveExternalStorageServiceAsUser(UserHandle.USER_SYSTEM); 375 if (serviceInfo == null) { 376 throw new ExternalStorageServiceException( 377 "No valid ExternalStorageService component found"); 378 } 379 380 ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); 381 if (!Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE 382 .equals(serviceInfo.permission)) { 383 throw new ExternalStorageServiceException(name.flattenToShortString() 384 + " does not require permission " 385 + Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE); 386 } 387 388 mExternalStorageServiceComponent = name; 389 } 390 391 /** Returns the {@link ExternalStorageService} component name. */ 392 @Nullable getExternalStorageServiceComponentName()393 public ComponentName getExternalStorageServiceComponentName() { 394 return mExternalStorageServiceComponent; 395 } 396 397 /** 398 * Notify the controller that an app with {@code uid} and {@code tid} is blocked on an IO 399 * request on {@code volumeUuid} for {@code reason}. 400 * 401 * This blocked state can be queried with {@link #isAppIoBlocked} 402 * 403 * @hide 404 */ notifyAppIoBlocked(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)405 public void notifyAppIoBlocked(String volumeUuid, int uid, int tid, 406 @StorageManager.AppIoBlockedReason int reason) { 407 final int userId = UserHandle.getUserId(uid); 408 final StorageUserConnection connection; 409 synchronized (mLock) { 410 connection = mConnections.get(userId); 411 } 412 413 if (connection != null) { 414 connection.notifyAppIoBlocked(volumeUuid, uid, tid, reason); 415 } 416 } 417 418 /** 419 * Notify the controller that an app with {@code uid} and {@code tid} has resmed a previously 420 * blocked IO request on {@code volumeUuid} for {@code reason}. 421 * 422 * All app IO will be automatically marked as unblocked if {@code volumeUuid} is unmounted. 423 */ notifyAppIoResumed(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)424 public void notifyAppIoResumed(String volumeUuid, int uid, int tid, 425 @StorageManager.AppIoBlockedReason int reason) { 426 final int userId = UserHandle.getUserId(uid); 427 final StorageUserConnection connection; 428 synchronized (mLock) { 429 connection = mConnections.get(userId); 430 } 431 432 if (connection != null) { 433 connection.notifyAppIoResumed(volumeUuid, uid, tid, reason); 434 } 435 } 436 437 /** Returns {@code true} if {@code uid} is blocked on IO, {@code false} otherwise */ isAppIoBlocked(int uid)438 public boolean isAppIoBlocked(int uid) { 439 final int userId = UserHandle.getUserId(uid); 440 final StorageUserConnection connection; 441 synchronized (mLock) { 442 connection = mConnections.get(userId); 443 } 444 445 if (connection != null) { 446 return connection.isAppIoBlocked(uid); 447 } 448 return false; 449 } 450 killExternalStorageService(int userId)451 private void killExternalStorageService(int userId) { 452 IActivityManager am = ActivityManager.getService(); 453 try { 454 am.killApplication(mExternalStorageServicePackageName, mExternalStorageServiceAppId, 455 userId, "storage_session_controller reset", ApplicationExitInfo.REASON_OTHER); 456 } catch (RemoteException e) { 457 Slog.i(TAG, "Failed to kill the ExtenalStorageService for user " + userId); 458 } 459 } 460 461 /** 462 * Returns {@code true} if {@code vol} is an emulated or visible public volume, 463 * {@code false} otherwise 464 **/ isEmulatedOrPublic(ImmutableVolumeInfo vol)465 public static boolean isEmulatedOrPublic(ImmutableVolumeInfo vol) { 466 return vol.getType() == VolumeInfo.TYPE_EMULATED 467 || (vol.getType() == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); 468 } 469 470 /** Exception thrown when communication with the {@link ExternalStorageService} fails. */ 471 public static class ExternalStorageServiceException extends Exception { ExternalStorageServiceException(Throwable cause)472 public ExternalStorageServiceException(Throwable cause) { 473 super(cause); 474 } 475 ExternalStorageServiceException(String message)476 public ExternalStorageServiceException(String message) { 477 super(message); 478 } 479 ExternalStorageServiceException(String message, Throwable cause)480 public ExternalStorageServiceException(String message, Throwable cause) { 481 super(message, cause); 482 } 483 } 484 isSupportedVolume(ImmutableVolumeInfo vol)485 private static boolean isSupportedVolume(ImmutableVolumeInfo vol) { 486 return isEmulatedOrPublic(vol) || vol.getType() == VolumeInfo.TYPE_STUB; 487 } 488 shouldHandle(@ullable ImmutableVolumeInfo vol)489 private boolean shouldHandle(@Nullable ImmutableVolumeInfo vol) { 490 return !mIsResetting && (vol == null || isSupportedVolume(vol)); 491 } 492 493 /** 494 * Returns {@code true} if the given user supports external storage, 495 * {@code false} otherwise. 496 */ supportsExternalStorage(int userId)497 public boolean supportsExternalStorage(int userId) { 498 return resolveExternalStorageServiceAsUser(userId) != null; 499 } 500 resolveExternalStorageServiceAsUser(int userId)501 private ServiceInfo resolveExternalStorageServiceAsUser(int userId) { 502 Intent intent = new Intent(ExternalStorageService.SERVICE_INTERFACE); 503 intent.setPackage(mExternalStorageServicePackageName); 504 ResolveInfo resolveInfo = mContext.getPackageManager().resolveServiceAsUser(intent, 505 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId); 506 if (resolveInfo == null) { 507 return null; 508 } 509 510 return resolveInfo.serviceInfo; 511 } 512 } 513