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