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