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