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 * Restarts all sessions for {@code userId}. 264 * 265 * Does nothing if {@link #shouldHandle} is {@code false} 266 * 267 * This call blocks and waits for all sessions to be started, however any failures when starting 268 * a session will be ignored. 269 */ onUnlockUser(int userId)270 public void onUnlockUser(int userId) throws ExternalStorageServiceException { 271 Slog.i(TAG, "On user unlock " + userId); 272 if (shouldHandle(null) && userId == 0) { 273 initExternalStorageServiceComponent(); 274 } 275 } 276 277 /** 278 * Called when a user is in the process is being stopped. 279 * 280 * Does nothing if {@link #shouldHandle} is {@code false} 281 * 282 * This call removes all sessions for the user that is being stopped; 283 * this will make sure that we don't rebind to the service needlessly. 284 */ onUserStopping(int userId)285 public void onUserStopping(int userId) { 286 if (!shouldHandle(null)) { 287 return; 288 } 289 StorageUserConnection connection = null; 290 synchronized (mLock) { 291 connection = mConnections.get(userId); 292 } 293 294 if (connection != null) { 295 Slog.i(TAG, "Removing all sessions for user: " + userId); 296 connection.removeAllSessions(); 297 } else { 298 Slog.w(TAG, "No connection found for user: " + userId); 299 } 300 } 301 302 /** 303 * Resets all sessions for all users and waits for exit. This may kill the 304 * {@link ExternalStorageservice} for a user if necessary to ensure all state has been reset. 305 * 306 * Does nothing if {@link #shouldHandle} is {@code false} 307 **/ onReset(IVold vold, Runnable resetHandlerRunnable)308 public void onReset(IVold vold, Runnable resetHandlerRunnable) { 309 if (!shouldHandle(null)) { 310 return; 311 } 312 313 SparseArray<StorageUserConnection> connections = new SparseArray(); 314 synchronized (mLock) { 315 mIsResetting = true; 316 Slog.i(TAG, "Started resetting external storage service..."); 317 for (int i = 0; i < mConnections.size(); i++) { 318 connections.put(mConnections.keyAt(i), mConnections.valueAt(i)); 319 } 320 } 321 322 for (int i = 0; i < connections.size(); i++) { 323 StorageUserConnection connection = connections.valueAt(i); 324 for (String sessionId : connection.getAllSessionIds()) { 325 try { 326 Slog.i(TAG, "Unmounting " + sessionId); 327 vold.unmount(sessionId); 328 Slog.i(TAG, "Unmounted " + sessionId); 329 } catch (ServiceSpecificException | RemoteException e) { 330 // TODO(b/140025078): Hard reset vold? 331 Slog.e(TAG, "Failed to unmount volume: " + sessionId, e); 332 } 333 334 try { 335 Slog.i(TAG, "Exiting " + sessionId); 336 connection.removeSessionAndWait(sessionId); 337 Slog.i(TAG, "Exited " + sessionId); 338 } catch (IllegalStateException | ExternalStorageServiceException e) { 339 Slog.e(TAG, "Failed to exit session: " + sessionId 340 + ". Killing MediaProvider...", e); 341 // If we failed to confirm the session exited, it is risky to proceed 342 // We kill the ExternalStorageService as a last resort 343 killExternalStorageService(connections.keyAt(i)); 344 break; 345 } 346 } 347 connection.close(); 348 } 349 350 resetHandlerRunnable.run(); 351 synchronized (mLock) { 352 mConnections.clear(); 353 mIsResetting = false; 354 Slog.i(TAG, "Finished resetting external storage service"); 355 } 356 } 357 initExternalStorageServiceComponent()358 private void initExternalStorageServiceComponent() throws ExternalStorageServiceException { 359 Slog.i(TAG, "Initialialising..."); 360 ProviderInfo provider = mContext.getPackageManager().resolveContentProvider( 361 MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE 362 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 363 | PackageManager.MATCH_SYSTEM_ONLY); 364 if (provider == null) { 365 throw new ExternalStorageServiceException("No valid MediaStore provider found"); 366 } 367 368 mExternalStorageServicePackageName = provider.applicationInfo.packageName; 369 mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid); 370 371 Intent intent = new Intent(ExternalStorageService.SERVICE_INTERFACE); 372 intent.setPackage(mExternalStorageServicePackageName); 373 ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, 374 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); 375 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 376 throw new ExternalStorageServiceException( 377 "No valid ExternalStorageService component found"); 378 } 379 380 ServiceInfo serviceInfo = resolveInfo.serviceInfo; 381 ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); 382 if (!Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE 383 .equals(serviceInfo.permission)) { 384 throw new ExternalStorageServiceException(name.flattenToShortString() 385 + " does not require permission " 386 + Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE); 387 } 388 389 mExternalStorageServiceComponent = name; 390 } 391 392 /** Returns the {@link ExternalStorageService} component name. */ 393 @Nullable getExternalStorageServiceComponentName()394 public ComponentName getExternalStorageServiceComponentName() { 395 return mExternalStorageServiceComponent; 396 } 397 398 /** 399 * Notify the controller that an app with {@code uid} and {@code tid} is blocked on an IO 400 * request on {@code volumeUuid} for {@code reason}. 401 * 402 * This blocked state can be queried with {@link #isAppIoBlocked} 403 * 404 * @hide 405 */ notifyAppIoBlocked(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)406 public void notifyAppIoBlocked(String volumeUuid, int uid, int tid, 407 @StorageManager.AppIoBlockedReason int reason) { 408 final int userId = UserHandle.getUserId(uid); 409 final StorageUserConnection connection; 410 synchronized (mLock) { 411 connection = mConnections.get(userId); 412 } 413 414 if (connection != null) { 415 connection.notifyAppIoBlocked(volumeUuid, uid, tid, reason); 416 } 417 } 418 419 /** 420 * Notify the controller that an app with {@code uid} and {@code tid} has resmed a previously 421 * blocked IO request on {@code volumeUuid} for {@code reason}. 422 * 423 * All app IO will be automatically marked as unblocked if {@code volumeUuid} is unmounted. 424 */ notifyAppIoResumed(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)425 public void notifyAppIoResumed(String volumeUuid, int uid, int tid, 426 @StorageManager.AppIoBlockedReason int reason) { 427 final int userId = UserHandle.getUserId(uid); 428 final StorageUserConnection connection; 429 synchronized (mLock) { 430 connection = mConnections.get(userId); 431 } 432 433 if (connection != null) { 434 connection.notifyAppIoResumed(volumeUuid, uid, tid, reason); 435 } 436 } 437 438 /** Returns {@code true} if {@code uid} is blocked on IO, {@code false} otherwise */ isAppIoBlocked(int uid)439 public boolean isAppIoBlocked(int uid) { 440 final int userId = UserHandle.getUserId(uid); 441 final StorageUserConnection connection; 442 synchronized (mLock) { 443 connection = mConnections.get(userId); 444 } 445 446 if (connection != null) { 447 return connection.isAppIoBlocked(uid); 448 } 449 return false; 450 } 451 killExternalStorageService(int userId)452 private void killExternalStorageService(int userId) { 453 IActivityManager am = ActivityManager.getService(); 454 try { 455 am.killApplication(mExternalStorageServicePackageName, mExternalStorageServiceAppId, 456 userId, "storage_session_controller reset"); 457 } catch (RemoteException e) { 458 Slog.i(TAG, "Failed to kill the ExtenalStorageService for user " + userId); 459 } 460 } 461 462 /** 463 * Returns {@code true} if {@code vol} is an emulated or visible public volume, 464 * {@code false} otherwise 465 **/ isEmulatedOrPublic(VolumeInfo vol)466 public static boolean isEmulatedOrPublic(VolumeInfo vol) { 467 return vol.type == VolumeInfo.TYPE_EMULATED 468 || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); 469 } 470 471 /** Exception thrown when communication with the {@link ExternalStorageService} fails. */ 472 public static class ExternalStorageServiceException extends Exception { ExternalStorageServiceException(Throwable cause)473 public ExternalStorageServiceException(Throwable cause) { 474 super(cause); 475 } 476 ExternalStorageServiceException(String message)477 public ExternalStorageServiceException(String message) { 478 super(message); 479 } 480 ExternalStorageServiceException(String message, Throwable cause)481 public ExternalStorageServiceException(String message, Throwable cause) { 482 super(message, cause); 483 } 484 } 485 isSupportedVolume(VolumeInfo vol)486 private static boolean isSupportedVolume(VolumeInfo vol) { 487 return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB; 488 } 489 shouldHandle(@ullable VolumeInfo vol)490 private boolean shouldHandle(@Nullable VolumeInfo vol) { 491 return !mIsResetting && (vol == null || isSupportedVolume(vol)); 492 } 493 } 494