| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.storage; |
| |
| import android.Manifest; |
| import android.annotation.Nullable; |
| import android.app.ActivityManager; |
| import android.app.IActivityManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ProviderInfo; |
| import android.content.pm.ResolveInfo; |
| import android.content.pm.ServiceInfo; |
| import android.os.IVold; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.os.ServiceSpecificException; |
| import android.os.UserHandle; |
| import android.os.storage.VolumeInfo; |
| import android.provider.MediaStore; |
| import android.service.storage.ExternalStorageService; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.util.Objects; |
| |
| /** |
| * Controls storage sessions for users initiated by the {@link StorageManagerService}. |
| * Each user on the device will be represented by a {@link StorageUserConnection}. |
| */ |
| public final class StorageSessionController { |
| private static final String TAG = "StorageSessionController"; |
| |
| private final Object mLock = new Object(); |
| private final Context mContext; |
| @GuardedBy("mLock") |
| private final SparseArray<StorageUserConnection> mConnections = new SparseArray<>(); |
| private final boolean mIsFuseEnabled; |
| |
| private volatile ComponentName mExternalStorageServiceComponent; |
| private volatile String mExternalStorageServicePackageName; |
| private volatile int mExternalStorageServiceAppId; |
| private volatile boolean mIsResetting; |
| |
| public StorageSessionController(Context context, boolean isFuseEnabled) { |
| mContext = Objects.requireNonNull(context); |
| mIsFuseEnabled = isFuseEnabled; |
| } |
| |
| /** |
| * Creates and starts a storage session associated with {@code deviceFd} for {@code vol}. |
| * Sessions can be started with {@link #onVolumeReady} and removed with {@link #onVolumeUnmount} |
| * or {@link #onVolumeRemove}. |
| * |
| * Throws an {@link IllegalStateException} if a session for {@code vol} has already been created |
| * |
| * Does nothing if {@link #shouldHandle} is {@code false} |
| * |
| * Blocks until the session is started or fails |
| * |
| * @throws ExternalStorageServiceException if the session fails to start |
| * @throws IllegalStateException if a session has already been created for {@code vol} |
| */ |
| public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol) |
| throws ExternalStorageServiceException { |
| if (!shouldHandle(vol)) { |
| return; |
| } |
| |
| Slog.i(TAG, "On volume mount " + vol); |
| |
| String sessionId = vol.getId(); |
| int userId = vol.getMountUserId(); |
| |
| StorageUserConnection connection = null; |
| synchronized (mLock) { |
| connection = mConnections.get(userId); |
| if (connection == null) { |
| Slog.i(TAG, "Creating connection for user: " + userId); |
| connection = new StorageUserConnection(mContext, userId, this); |
| mConnections.put(userId, connection); |
| } |
| Slog.i(TAG, "Creating and starting session with id: " + sessionId); |
| connection.startSession(sessionId, deviceFd, vol.getPath().getPath(), |
| vol.getInternalPath().getPath()); |
| } |
| } |
| |
| /** |
| * Notifies the Storage Service that volume state for {@code vol} is changed. |
| * A session may already be created for this volume if it is mounted before or the volume state |
| * has changed to mounted. |
| * |
| * Does nothing if {@link #shouldHandle} is {@code false} |
| * |
| * Blocks until the Storage Service processes/scans the volume or fails in doing so. |
| * |
| * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService |
| */ |
| public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException { |
| if (!shouldHandle(vol)) { |
| return; |
| } |
| String sessionId = vol.getId(); |
| int userId = vol.getMountUserId(); |
| |
| StorageUserConnection connection = null; |
| synchronized (mLock) { |
| connection = mConnections.get(userId); |
| if (connection != null) { |
| Slog.i(TAG, "Notifying volume state changed for session with id: " + sessionId); |
| connection.notifyVolumeStateChanged(sessionId, |
| vol.buildStorageVolume(mContext, userId, false)); |
| } else { |
| Slog.w(TAG, "No available storage user connection for userId : " + userId); |
| } |
| } |
| } |
| |
| |
| /** |
| * Removes and returns the {@link StorageUserConnection} for {@code vol}. |
| * |
| * Does nothing if {@link #shouldHandle} is {@code false} |
| * |
| * @return the connection that was removed or {@code null} if nothing was removed |
| */ |
| @Nullable |
| public StorageUserConnection onVolumeRemove(VolumeInfo vol) { |
| if (!shouldHandle(vol)) { |
| return null; |
| } |
| |
| Slog.i(TAG, "On volume remove " + vol); |
| String sessionId = vol.getId(); |
| int userId = vol.getMountUserId(); |
| |
| synchronized (mLock) { |
| StorageUserConnection connection = mConnections.get(userId); |
| if (connection != null) { |
| Slog.i(TAG, "Removed session for vol with id: " + sessionId); |
| connection.removeSession(sessionId); |
| return connection; |
| } else { |
| Slog.w(TAG, "Session already removed for vol with id: " + sessionId); |
| return null; |
| } |
| } |
| } |
| |
| |
| /** |
| * Removes a storage session for {@code vol} and waits for exit. |
| * |
| * Does nothing if {@link #shouldHandle} is {@code false} |
| * |
| * Any errors are ignored |
| * |
| * Call {@link #onVolumeRemove} to remove the connection without waiting for exit |
| */ |
| public void onVolumeUnmount(VolumeInfo vol) { |
| StorageUserConnection connection = onVolumeRemove(vol); |
| |
| Slog.i(TAG, "On volume unmount " + vol); |
| if (connection != null) { |
| String sessionId = vol.getId(); |
| |
| try { |
| connection.removeSessionAndWait(sessionId); |
| } catch (ExternalStorageServiceException e) { |
| Slog.e(TAG, "Failed to end session for vol with id: " + sessionId, e); |
| } |
| } |
| } |
| |
| /** |
| * Restarts all sessions for {@code userId}. |
| * |
| * Does nothing if {@link #shouldHandle} is {@code false} |
| * |
| * This call blocks and waits for all sessions to be started, however any failures when starting |
| * a session will be ignored. |
| */ |
| public void onUnlockUser(int userId) throws ExternalStorageServiceException { |
| Slog.i(TAG, "On user unlock " + userId); |
| if (shouldHandle(null) && userId == 0) { |
| initExternalStorageServiceComponent(); |
| } |
| } |
| |
| /** |
| * Called when a user is in the process is being stopped. |
| * |
| * Does nothing if {@link #shouldHandle} is {@code false} |
| * |
| * This call removes all sessions for the user that is being stopped; |
| * this will make sure that we don't rebind to the service needlessly. |
| */ |
| public void onUserStopping(int userId) { |
| if (!shouldHandle(null)) { |
| return; |
| } |
| StorageUserConnection connection = null; |
| synchronized (mLock) { |
| connection = mConnections.get(userId); |
| } |
| |
| if (connection != null) { |
| Slog.i(TAG, "Removing all sessions for user: " + userId); |
| connection.removeAllSessions(); |
| } else { |
| Slog.w(TAG, "No connection found for user: " + userId); |
| } |
| } |
| |
| /** |
| * Resets all sessions for all users and waits for exit. This may kill the |
| * {@link ExternalStorageservice} for a user if necessary to ensure all state has been reset. |
| * |
| * Does nothing if {@link #shouldHandle} is {@code false} |
| **/ |
| public void onReset(IVold vold, Runnable resetHandlerRunnable) { |
| if (!shouldHandle(null)) { |
| return; |
| } |
| |
| SparseArray<StorageUserConnection> connections = new SparseArray(); |
| synchronized (mLock) { |
| mIsResetting = true; |
| Slog.i(TAG, "Started resetting external storage service..."); |
| for (int i = 0; i < mConnections.size(); i++) { |
| connections.put(mConnections.keyAt(i), mConnections.valueAt(i)); |
| } |
| } |
| |
| for (int i = 0; i < connections.size(); i++) { |
| StorageUserConnection connection = connections.valueAt(i); |
| for (String sessionId : connection.getAllSessionIds()) { |
| try { |
| Slog.i(TAG, "Unmounting " + sessionId); |
| vold.unmount(sessionId); |
| Slog.i(TAG, "Unmounted " + sessionId); |
| } catch (ServiceSpecificException | RemoteException e) { |
| // TODO(b/140025078): Hard reset vold? |
| Slog.e(TAG, "Failed to unmount volume: " + sessionId, e); |
| } |
| |
| try { |
| Slog.i(TAG, "Exiting " + sessionId); |
| connection.removeSessionAndWait(sessionId); |
| Slog.i(TAG, "Exited " + sessionId); |
| } catch (IllegalStateException | ExternalStorageServiceException e) { |
| Slog.e(TAG, "Failed to exit session: " + sessionId |
| + ". Killing MediaProvider...", e); |
| // If we failed to confirm the session exited, it is risky to proceed |
| // We kill the ExternalStorageService as a last resort |
| killExternalStorageService(connections.keyAt(i)); |
| break; |
| } |
| } |
| connection.close(); |
| } |
| |
| resetHandlerRunnable.run(); |
| synchronized (mLock) { |
| mConnections.clear(); |
| mIsResetting = false; |
| Slog.i(TAG, "Finished resetting external storage service"); |
| } |
| } |
| |
| private void initExternalStorageServiceComponent() throws ExternalStorageServiceException { |
| Slog.i(TAG, "Initialialising..."); |
| ProviderInfo provider = mContext.getPackageManager().resolveContentProvider( |
| MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE |
| | PackageManager.MATCH_DIRECT_BOOT_UNAWARE |
| | PackageManager.MATCH_SYSTEM_ONLY); |
| if (provider == null) { |
| throw new ExternalStorageServiceException("No valid MediaStore provider found"); |
| } |
| |
| mExternalStorageServicePackageName = provider.applicationInfo.packageName; |
| mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid); |
| |
| Intent intent = new Intent(ExternalStorageService.SERVICE_INTERFACE); |
| intent.setPackage(mExternalStorageServicePackageName); |
| ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, |
| PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); |
| if (resolveInfo == null || resolveInfo.serviceInfo == null) { |
| throw new ExternalStorageServiceException( |
| "No valid ExternalStorageService component found"); |
| } |
| |
| ServiceInfo serviceInfo = resolveInfo.serviceInfo; |
| ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); |
| if (!Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE |
| .equals(serviceInfo.permission)) { |
| throw new ExternalStorageServiceException(name.flattenToShortString() |
| + " does not require permission " |
| + Manifest.permission.BIND_EXTERNAL_STORAGE_SERVICE); |
| } |
| |
| mExternalStorageServiceComponent = name; |
| } |
| |
| /** Returns the {@link ExternalStorageService} component name. */ |
| @Nullable |
| public ComponentName getExternalStorageServiceComponentName() { |
| return mExternalStorageServiceComponent; |
| } |
| |
| private void killExternalStorageService(int userId) { |
| IActivityManager am = ActivityManager.getService(); |
| try { |
| am.killApplication(mExternalStorageServicePackageName, mExternalStorageServiceAppId, |
| userId, "storage_session_controller reset"); |
| } catch (RemoteException e) { |
| Slog.i(TAG, "Failed to kill the ExtenalStorageService for user " + userId); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if {@code vol} is an emulated or visible public volume, |
| * {@code false} otherwise |
| **/ |
| public static boolean isEmulatedOrPublic(VolumeInfo vol) { |
| return vol.type == VolumeInfo.TYPE_EMULATED |
| || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); |
| } |
| |
| /** Exception thrown when communication with the {@link ExternalStorageService} fails. */ |
| public static class ExternalStorageServiceException extends Exception { |
| public ExternalStorageServiceException(Throwable cause) { |
| super(cause); |
| } |
| |
| public ExternalStorageServiceException(String message) { |
| super(message); |
| } |
| |
| public ExternalStorageServiceException(String message, Throwable cause) { |
| super(message, cause); |
| } |
| } |
| |
| private boolean shouldHandle(@Nullable VolumeInfo vol) { |
| return mIsFuseEnabled && !mIsResetting && (vol == null || isEmulatedOrPublic(vol)); |
| } |
| } |