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.os.IVold; 31 import android.os.ParcelFileDescriptor; 32 import android.os.RemoteException; 33 import android.os.ServiceSpecificException; 34 import android.os.UserHandle; 35 import android.os.storage.VolumeInfo; 36 import android.provider.MediaStore; 37 import android.service.storage.ExternalStorageService; 38 import android.util.Slog; 39 import android.util.SparseArray; 40 41 import com.android.internal.annotations.GuardedBy; 42 43 import java.util.Objects; 44 45 /** 46 * Controls storage sessions for users initiated by the {@link StorageManagerService}. 47 * Each user on the device will be represented by a {@link StorageUserConnection}. 48 */ 49 public final class StorageSessionController { 50 private static final String TAG = "StorageSessionController"; 51 52 private final Object mLock = new Object(); 53 private final Context mContext; 54 @GuardedBy("mLock") 55 private final SparseArray<StorageUserConnection> mConnections = new SparseArray<>(); 56 private final boolean mIsFuseEnabled; 57 58 private volatile ComponentName mExternalStorageServiceComponent; 59 private volatile String mExternalStorageServicePackageName; 60 private volatile int mExternalStorageServiceAppId; 61 private volatile boolean mIsResetting; 62 StorageSessionController(Context context, boolean isFuseEnabled)63 public StorageSessionController(Context context, boolean isFuseEnabled) { 64 mContext = Objects.requireNonNull(context); 65 mIsFuseEnabled = isFuseEnabled; 66 } 67 68 /** 69 * Creates and starts a storage session associated with {@code deviceFd} for {@code vol}. 70 * Sessions can be started with {@link #onVolumeReady} and removed with {@link #onVolumeUnmount} 71 * or {@link #onVolumeRemove}. 72 * 73 * Throws an {@link IllegalStateException} if a session for {@code vol} has already been created 74 * 75 * Does nothing if {@link #shouldHandle} is {@code false} 76 * 77 * Blocks until the session is started or fails 78 * 79 * @throws ExternalStorageServiceException if the session fails to start 80 * @throws IllegalStateException if a session has already been created for {@code vol} 81 */ onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol)82 public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol) 83 throws ExternalStorageServiceException { 84 if (!shouldHandle(vol)) { 85 return; 86 } 87 88 Slog.i(TAG, "On volume mount " + vol); 89 90 String sessionId = vol.getId(); 91 int userId = vol.getMountUserId(); 92 93 StorageUserConnection connection = null; 94 synchronized (mLock) { 95 connection = mConnections.get(userId); 96 if (connection == null) { 97 Slog.i(TAG, "Creating connection for user: " + userId); 98 connection = new StorageUserConnection(mContext, userId, this); 99 mConnections.put(userId, connection); 100 } 101 Slog.i(TAG, "Creating and starting session with id: " + sessionId); 102 connection.startSession(sessionId, deviceFd, vol.getPath().getPath(), 103 vol.getInternalPath().getPath()); 104 } 105 } 106 107 /** 108 * Notifies the Storage Service that volume state for {@code vol} is changed. 109 * A session may already be created for this volume if it is mounted before or the volume state 110 * has changed to mounted. 111 * 112 * Does nothing if {@link #shouldHandle} is {@code false} 113 * 114 * Blocks until the Storage Service processes/scans the volume or fails in doing so. 115 * 116 * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService 117 */ notifyVolumeStateChanged(VolumeInfo vol)118 public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException { 119 if (!shouldHandle(vol)) { 120 return; 121 } 122 String sessionId = vol.getId(); 123 int userId = vol.getMountUserId(); 124 125 StorageUserConnection connection = null; 126 synchronized (mLock) { 127 connection = mConnections.get(userId); 128 if (connection != null) { 129 Slog.i(TAG, "Notifying volume state changed for session with id: " + sessionId); 130 connection.notifyVolumeStateChanged(sessionId, 131 vol.buildStorageVolume(mContext, userId, false)); 132 } else { 133 Slog.w(TAG, "No available storage user connection for userId : " + userId); 134 } 135 } 136 } 137 138 139 /** 140 * Removes and returns the {@link StorageUserConnection} for {@code vol}. 141 * 142 * Does nothing if {@link #shouldHandle} is {@code false} 143 * 144 * @return the connection that was removed or {@code null} if nothing was removed 145 */ 146 @Nullable onVolumeRemove(VolumeInfo vol)147 public StorageUserConnection onVolumeRemove(VolumeInfo vol) { 148 if (!shouldHandle(vol)) { 149 return null; 150 } 151 152 Slog.i(TAG, "On volume remove " + vol); 153 String sessionId = vol.getId(); 154 int userId = vol.getMountUserId(); 155 156 synchronized (mLock) { 157 StorageUserConnection connection = mConnections.get(userId); 158 if (connection != null) { 159 Slog.i(TAG, "Removed session for vol with id: " + sessionId); 160 connection.removeSession(sessionId); 161 return connection; 162 } else { 163 Slog.w(TAG, "Session already removed for vol with id: " + sessionId); 164 return null; 165 } 166 } 167 } 168 169 170 /** 171 * Removes a storage session for {@code vol} and waits for exit. 172 * 173 * Does nothing if {@link #shouldHandle} is {@code false} 174 * 175 * Any errors are ignored 176 * 177 * Call {@link #onVolumeRemove} to remove the connection without waiting for exit 178 */ onVolumeUnmount(VolumeInfo vol)179 public void onVolumeUnmount(VolumeInfo vol) { 180 StorageUserConnection connection = onVolumeRemove(vol); 181 182 Slog.i(TAG, "On volume unmount " + vol); 183 if (connection != null) { 184 String sessionId = vol.getId(); 185 186 try { 187 connection.removeSessionAndWait(sessionId); 188 } catch (ExternalStorageServiceException e) { 189 Slog.e(TAG, "Failed to end session for vol with id: " + sessionId, e); 190 } 191 } 192 } 193 194 /** 195 * Restarts all sessions for {@code userId}. 196 * 197 * Does nothing if {@link #shouldHandle} is {@code false} 198 * 199 * This call blocks and waits for all sessions to be started, however any failures when starting 200 * a session will be ignored. 201 */ onUnlockUser(int userId)202 public void onUnlockUser(int userId) throws ExternalStorageServiceException { 203 Slog.i(TAG, "On user unlock " + userId); 204 if (shouldHandle(null) && userId == 0) { 205 initExternalStorageServiceComponent(); 206 } 207 } 208 209 /** 210 * Called when a user is in the process is being stopped. 211 * 212 * Does nothing if {@link #shouldHandle} is {@code false} 213 * 214 * This call removes all sessions for the user that is being stopped; 215 * this will make sure that we don't rebind to the service needlessly. 216 */ onUserStopping(int userId)217 public void onUserStopping(int userId) { 218 if (!shouldHandle(null)) { 219 return; 220 } 221 StorageUserConnection connection = null; 222 synchronized (mLock) { 223 connection = mConnections.get(userId); 224 } 225 226 if (connection != null) { 227 Slog.i(TAG, "Removing all sessions for user: " + userId); 228 connection.removeAllSessions(); 229 } else { 230 Slog.w(TAG, "No connection found for user: " + userId); 231 } 232 } 233 234 /** 235 * Resets all sessions for all users and waits for exit. This may kill the 236 * {@link ExternalStorageservice} for a user if necessary to ensure all state has been reset. 237 * 238 * Does nothing if {@link #shouldHandle} is {@code false} 239 **/ onReset(IVold vold, Runnable resetHandlerRunnable)240 public void onReset(IVold vold, Runnable resetHandlerRunnable) { 241 if (!shouldHandle(null)) { 242 return; 243 } 244 245 SparseArray<StorageUserConnection> connections = new SparseArray(); 246 synchronized (mLock) { 247 mIsResetting = true; 248 Slog.i(TAG, "Started resetting external storage service..."); 249 for (int i = 0; i < mConnections.size(); i++) { 250 connections.put(mConnections.keyAt(i), mConnections.valueAt(i)); 251 } 252 } 253 254 for (int i = 0; i < connections.size(); i++) { 255 StorageUserConnection connection = connections.valueAt(i); 256 for (String sessionId : connection.getAllSessionIds()) { 257 try { 258 Slog.i(TAG, "Unmounting " + sessionId); 259 vold.unmount(sessionId); 260 Slog.i(TAG, "Unmounted " + sessionId); 261 } catch (ServiceSpecificException | RemoteException e) { 262 // TODO(b/140025078): Hard reset vold? 263 Slog.e(TAG, "Failed to unmount volume: " + sessionId, e); 264 } 265 266 try { 267 Slog.i(TAG, "Exiting " + sessionId); 268 connection.removeSessionAndWait(sessionId); 269 Slog.i(TAG, "Exited " + sessionId); 270 } catch (IllegalStateException | ExternalStorageServiceException e) { 271 Slog.e(TAG, "Failed to exit session: " + sessionId 272 + ". Killing MediaProvider...", e); 273 // If we failed to confirm the session exited, it is risky to proceed 274 // We kill the ExternalStorageService as a last resort 275 killExternalStorageService(connections.keyAt(i)); 276 break; 277 } 278 } 279 connection.close(); 280 } 281 282 resetHandlerRunnable.run(); 283 synchronized (mLock) { 284 mConnections.clear(); 285 mIsResetting = false; 286 Slog.i(TAG, "Finished resetting external storage service"); 287 } 288 } 289 initExternalStorageServiceComponent()290 private void initExternalStorageServiceComponent() throws ExternalStorageServiceException { 291 Slog.i(TAG, "Initialialising..."); 292 ProviderInfo provider = mContext.getPackageManager().resolveContentProvider( 293 MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE 294 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 295 | PackageManager.MATCH_SYSTEM_ONLY); 296 if (provider == null) { 297 throw new ExternalStorageServiceException("No valid MediaStore provider found"); 298 } 299 300 mExternalStorageServicePackageName = provider.applicationInfo.packageName; 301 mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid); 302 303 Intent intent = new Intent(ExternalStorageService.SERVICE_INTERFACE); 304 intent.setPackage(mExternalStorageServicePackageName); 305 ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, 306 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); 307 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 308 throw new ExternalStorageServiceException( 309 "No valid ExternalStorageService component found"); 310 } 311 312 ServiceInfo serviceInfo = resolveInfo.serviceInfo; 313 ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); 314 if (!Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE 315 .equals(serviceInfo.permission)) { 316 throw new ExternalStorageServiceException(name.flattenToShortString() 317 + " does not require permission " 318 + Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE); 319 } 320 321 mExternalStorageServiceComponent = name; 322 } 323 324 /** Returns the {@link ExternalStorageService} component name. */ 325 @Nullable getExternalStorageServiceComponentName()326 public ComponentName getExternalStorageServiceComponentName() { 327 return mExternalStorageServiceComponent; 328 } 329 killExternalStorageService(int userId)330 private void killExternalStorageService(int userId) { 331 IActivityManager am = ActivityManager.getService(); 332 try { 333 am.killApplication(mExternalStorageServicePackageName, mExternalStorageServiceAppId, 334 userId, "storage_session_controller reset"); 335 } catch (RemoteException e) { 336 Slog.i(TAG, "Failed to kill the ExtenalStorageService for user " + userId); 337 } 338 } 339 340 /** 341 * Returns {@code true} if {@code vol} is an emulated or public volume, 342 * {@code false} otherwise 343 **/ isEmulatedOrPublic(VolumeInfo vol)344 public static boolean isEmulatedOrPublic(VolumeInfo vol) { 345 return vol.type == VolumeInfo.TYPE_EMULATED || vol.type == VolumeInfo.TYPE_PUBLIC; 346 } 347 348 /** Exception thrown when communication with the {@link ExternalStorageService} fails. */ 349 public static class ExternalStorageServiceException extends Exception { ExternalStorageServiceException(Throwable cause)350 public ExternalStorageServiceException(Throwable cause) { 351 super(cause); 352 } 353 ExternalStorageServiceException(String message)354 public ExternalStorageServiceException(String message) { 355 super(message); 356 } 357 ExternalStorageServiceException(String message, Throwable cause)358 public ExternalStorageServiceException(String message, Throwable cause) { 359 super(message, cause); 360 } 361 } 362 shouldHandle(@ullable VolumeInfo vol)363 private boolean shouldHandle(@Nullable VolumeInfo vol) { 364 return mIsFuseEnabled && !mIsResetting && (vol == null || isEmulatedOrPublic(vol)); 365 } 366 } 367