blob: 0d059ae389e9cd96ee9c35bc8abea16edc19acb8 [file] [log] [blame]
/*
* 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));
}
}