diff options
Diffstat (limited to 'services/core/java/com/android/server/MountService.java')
-rw-r--r-- | services/core/java/com/android/server/MountService.java | 2833 |
1 files changed, 2833 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java new file mode 100644 index 0000000..e60231a --- /dev/null +++ b/services/core/java/com/android/server/MountService.java @@ -0,0 +1,2833 @@ +/* + * Copyright (C) 2007 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; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import android.Manifest; +import android.app.AppOpsManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.content.res.ObbInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.hardware.usb.UsbManager; +import android.net.Uri; +import android.os.Binder; +import android.os.Environment; +import android.os.Environment.UserEnvironment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.storage.IMountService; +import android.os.storage.IMountServiceListener; +import android.os.storage.IMountShutdownObserver; +import android.os.storage.IObbActionListener; +import android.os.storage.OnObbStateChangeListener; +import android.os.storage.StorageResultCode; +import android.os.storage.StorageVolume; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IMediaContainerService; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; +import com.android.server.NativeDaemonConnector.Command; +import com.android.server.NativeDaemonConnector.SensitiveArg; +import com.android.server.am.ActivityManagerService; +import com.android.server.pm.PackageManagerService; +import com.android.server.pm.UserManagerService; +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** + * MountService implements back-end services for platform storage + * management. + * @hide - Applications should use android.os.storage.StorageManager + * to access the MountService. + */ +class MountService extends IMountService.Stub + implements INativeDaemonConnectorCallbacks, Watchdog.Monitor { + + // TODO: listen for user creation/deletion + + private static final boolean LOCAL_LOGD = false; + private static final boolean DEBUG_UNMOUNT = false; + private static final boolean DEBUG_EVENTS = false; + private static final boolean DEBUG_OBB = false; + + // Disable this since it messes up long-running cryptfs operations. + private static final boolean WATCHDOG_ENABLE = false; + + private static final String TAG = "MountService"; + + private static final String VOLD_TAG = "VoldConnector"; + + /** Maximum number of ASEC containers allowed to be mounted. */ + private static final int MAX_CONTAINERS = 250; + + /* + * Internal vold volume state constants + */ + class VolumeState { + public static final int Init = -1; + public static final int NoMedia = 0; + public static final int Idle = 1; + public static final int Pending = 2; + public static final int Checking = 3; + public static final int Mounted = 4; + public static final int Unmounting = 5; + public static final int Formatting = 6; + public static final int Shared = 7; + public static final int SharedMnt = 8; + } + + /* + * Internal vold response code constants + */ + class VoldResponseCode { + /* + * 100 series - Requestion action was initiated; expect another reply + * before proceeding with a new command. + */ + public static final int VolumeListResult = 110; + public static final int AsecListResult = 111; + public static final int StorageUsersListResult = 112; + + /* + * 200 series - Requestion action has been successfully completed. + */ + public static final int ShareStatusResult = 210; + public static final int AsecPathResult = 211; + public static final int ShareEnabledResult = 212; + + /* + * 400 series - Command was accepted, but the requested action + * did not take place. + */ + public static final int OpFailedNoMedia = 401; + public static final int OpFailedMediaBlank = 402; + public static final int OpFailedMediaCorrupt = 403; + public static final int OpFailedVolNotMounted = 404; + public static final int OpFailedStorageBusy = 405; + public static final int OpFailedStorageNotFound = 406; + + /* + * 600 series - Unsolicited broadcasts. + */ + public static final int VolumeStateChange = 605; + public static final int VolumeUuidChange = 613; + public static final int VolumeUserLabelChange = 614; + public static final int VolumeDiskInserted = 630; + public static final int VolumeDiskRemoved = 631; + public static final int VolumeBadRemoval = 632; + + /* + * 700 series - fstrim + */ + public static final int FstrimCompleted = 700; + } + + private Context mContext; + private NativeDaemonConnector mConnector; + + private final Object mVolumesLock = new Object(); + + /** When defined, base template for user-specific {@link StorageVolume}. */ + private StorageVolume mEmulatedTemplate; + + // TODO: separate storage volumes on per-user basis + + @GuardedBy("mVolumesLock") + private final ArrayList<StorageVolume> mVolumes = Lists.newArrayList(); + /** Map from path to {@link StorageVolume} */ + @GuardedBy("mVolumesLock") + private final HashMap<String, StorageVolume> mVolumesByPath = Maps.newHashMap(); + /** Map from path to state */ + @GuardedBy("mVolumesLock") + private final HashMap<String, String> mVolumeStates = Maps.newHashMap(); + + private volatile boolean mSystemReady = false; + + private PackageManagerService mPms; + private boolean mUmsEnabling; + private boolean mUmsAvailable = false; + // Used as a lock for methods that register/unregister listeners. + final private ArrayList<MountServiceBinderListener> mListeners = + new ArrayList<MountServiceBinderListener>(); + private final CountDownLatch mConnectedSignal = new CountDownLatch(1); + private final CountDownLatch mAsecsScanned = new CountDownLatch(1); + private boolean mSendUmsConnectedOnBoot = false; + + /** + * Private hash of currently mounted secure containers. + * Used as a lock in methods to manipulate secure containers. + */ + final private HashSet<String> mAsecMountSet = new HashSet<String>(); + + /** + * The size of the crypto algorithm key in bits for OBB files. Currently + * Twofish is used which takes 128-bit keys. + */ + private static final int CRYPTO_ALGORITHM_KEY_SIZE = 128; + + /** + * The number of times to run SHA1 in the PBKDF2 function for OBB files. + * 1024 is reasonably secure and not too slow. + */ + private static final int PBKDF2_HASH_ROUNDS = 1024; + + /** + * Mounted OBB tracking information. Used to track the current state of all + * OBBs. + */ + final private Map<IBinder, List<ObbState>> mObbMounts = new HashMap<IBinder, List<ObbState>>(); + + /** Map from raw paths to {@link ObbState}. */ + final private Map<String, ObbState> mObbPathToStateMap = new HashMap<String, ObbState>(); + + class ObbState implements IBinder.DeathRecipient { + public ObbState(String rawPath, String canonicalPath, int callingUid, + IObbActionListener token, int nonce) { + this.rawPath = rawPath; + this.canonicalPath = canonicalPath.toString(); + + final int userId = UserHandle.getUserId(callingUid); + this.ownerPath = buildObbPath(canonicalPath, userId, false); + this.voldPath = buildObbPath(canonicalPath, userId, true); + + this.ownerGid = UserHandle.getSharedAppGid(callingUid); + this.token = token; + this.nonce = nonce; + } + + final String rawPath; + final String canonicalPath; + final String ownerPath; + final String voldPath; + + final int ownerGid; + + // Token of remote Binder caller + final IObbActionListener token; + + // Identifier to pass back to the token + final int nonce; + + public IBinder getBinder() { + return token.asBinder(); + } + + @Override + public void binderDied() { + ObbAction action = new UnmountObbAction(this, true); + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_RUN_ACTION, action)); + } + + public void link() throws RemoteException { + getBinder().linkToDeath(this, 0); + } + + public void unlink() { + getBinder().unlinkToDeath(this, 0); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ObbState{"); + sb.append("rawPath=").append(rawPath); + sb.append(",canonicalPath=").append(canonicalPath); + sb.append(",ownerPath=").append(ownerPath); + sb.append(",voldPath=").append(voldPath); + sb.append(",ownerGid=").append(ownerGid); + sb.append(",token=").append(token); + sb.append(",binder=").append(getBinder()); + sb.append('}'); + return sb.toString(); + } + } + + // OBB Action Handler + final private ObbActionHandler mObbActionHandler; + + // OBB action handler messages + private static final int OBB_RUN_ACTION = 1; + private static final int OBB_MCS_BOUND = 2; + private static final int OBB_MCS_UNBIND = 3; + private static final int OBB_MCS_RECONNECT = 4; + private static final int OBB_FLUSH_MOUNT_STATE = 5; + + /* + * Default Container Service information + */ + static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName( + "com.android.defcontainer", "com.android.defcontainer.DefaultContainerService"); + + final private DefaultContainerConnection mDefContainerConn = new DefaultContainerConnection(); + + class DefaultContainerConnection implements ServiceConnection { + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG_OBB) + Slog.i(TAG, "onServiceConnected"); + IMediaContainerService imcs = IMediaContainerService.Stub.asInterface(service); + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_MCS_BOUND, imcs)); + } + + public void onServiceDisconnected(ComponentName name) { + if (DEBUG_OBB) + Slog.i(TAG, "onServiceDisconnected"); + } + }; + + // Used in the ObbActionHandler + private IMediaContainerService mContainerService = null; + + // Handler messages + private static final int H_UNMOUNT_PM_UPDATE = 1; + private static final int H_UNMOUNT_PM_DONE = 2; + private static final int H_UNMOUNT_MS = 3; + private static final int H_SYSTEM_READY = 4; + + private static final int RETRY_UNMOUNT_DELAY = 30; // in ms + private static final int MAX_UNMOUNT_RETRIES = 4; + + class UnmountCallBack { + final String path; + final boolean force; + final boolean removeEncryption; + int retries; + + UnmountCallBack(String path, boolean force, boolean removeEncryption) { + retries = 0; + this.path = path; + this.force = force; + this.removeEncryption = removeEncryption; + } + + void handleFinished() { + if (DEBUG_UNMOUNT) Slog.i(TAG, "Unmounting " + path); + doUnmountVolume(path, true, removeEncryption); + } + } + + class UmsEnableCallBack extends UnmountCallBack { + final String method; + + UmsEnableCallBack(String path, String method, boolean force) { + super(path, force, false); + this.method = method; + } + + @Override + void handleFinished() { + super.handleFinished(); + doShareUnshareVolume(path, method, true); + } + } + + class ShutdownCallBack extends UnmountCallBack { + IMountShutdownObserver observer; + ShutdownCallBack(String path, IMountShutdownObserver observer) { + super(path, true, false); + this.observer = observer; + } + + @Override + void handleFinished() { + int ret = doUnmountVolume(path, true, removeEncryption); + if (observer != null) { + try { + observer.onShutDownComplete(ret); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException when shutting down"); + } + } + } + } + + class MountServiceHandler extends Handler { + ArrayList<UnmountCallBack> mForceUnmounts = new ArrayList<UnmountCallBack>(); + boolean mUpdatingStatus = false; + + MountServiceHandler(Looper l) { + super(l); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case H_UNMOUNT_PM_UPDATE: { + if (DEBUG_UNMOUNT) Slog.i(TAG, "H_UNMOUNT_PM_UPDATE"); + UnmountCallBack ucb = (UnmountCallBack) msg.obj; + mForceUnmounts.add(ucb); + if (DEBUG_UNMOUNT) Slog.i(TAG, " registered = " + mUpdatingStatus); + // Register only if needed. + if (!mUpdatingStatus) { + if (DEBUG_UNMOUNT) Slog.i(TAG, "Updating external media status on PackageManager"); + mUpdatingStatus = true; + mPms.updateExternalMediaStatus(false, true); + } + break; + } + case H_UNMOUNT_PM_DONE: { + if (DEBUG_UNMOUNT) Slog.i(TAG, "H_UNMOUNT_PM_DONE"); + if (DEBUG_UNMOUNT) Slog.i(TAG, "Updated status. Processing requests"); + mUpdatingStatus = false; + int size = mForceUnmounts.size(); + int sizeArr[] = new int[size]; + int sizeArrN = 0; + // Kill processes holding references first + ActivityManagerService ams = (ActivityManagerService) + ServiceManager.getService("activity"); + for (int i = 0; i < size; i++) { + UnmountCallBack ucb = mForceUnmounts.get(i); + String path = ucb.path; + boolean done = false; + if (!ucb.force) { + done = true; + } else { + int pids[] = getStorageUsers(path); + if (pids == null || pids.length == 0) { + done = true; + } else { + // Eliminate system process here? + ams.killPids(pids, "unmount media", true); + // Confirm if file references have been freed. + pids = getStorageUsers(path); + if (pids == null || pids.length == 0) { + done = true; + } + } + } + if (!done && (ucb.retries < MAX_UNMOUNT_RETRIES)) { + // Retry again + Slog.i(TAG, "Retrying to kill storage users again"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(H_UNMOUNT_PM_DONE, + ucb.retries++), + RETRY_UNMOUNT_DELAY); + } else { + if (ucb.retries >= MAX_UNMOUNT_RETRIES) { + Slog.i(TAG, "Failed to unmount media inspite of " + + MAX_UNMOUNT_RETRIES + " retries. Forcibly killing processes now"); + } + sizeArr[sizeArrN++] = i; + mHandler.sendMessage(mHandler.obtainMessage(H_UNMOUNT_MS, + ucb)); + } + } + // Remove already processed elements from list. + for (int i = (sizeArrN-1); i >= 0; i--) { + mForceUnmounts.remove(sizeArr[i]); + } + break; + } + case H_UNMOUNT_MS: { + if (DEBUG_UNMOUNT) Slog.i(TAG, "H_UNMOUNT_MS"); + UnmountCallBack ucb = (UnmountCallBack) msg.obj; + ucb.handleFinished(); + break; + } + case H_SYSTEM_READY: { + try { + handleSystemReady(); + } catch (Exception ex) { + Slog.e(TAG, "Boot-time mount exception", ex); + } + break; + } + } + } + }; + + private final Handler mHandler; + + void waitForAsecScan() { + waitForLatch(mAsecsScanned); + } + + private void waitForReady() { + waitForLatch(mConnectedSignal); + } + + private void waitForLatch(CountDownLatch latch) { + for (;;) { + try { + if (latch.await(5000, TimeUnit.MILLISECONDS)) { + return; + } else { + Slog.w(TAG, "Thread " + Thread.currentThread().getName() + + " still waiting for MountService ready..."); + } + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupt while waiting for MountService to be ready."); + } + } + } + + private void handleSystemReady() { + // Snapshot current volume states since it's not safe to call into vold + // while holding locks. + final HashMap<String, String> snapshot; + synchronized (mVolumesLock) { + snapshot = new HashMap<String, String>(mVolumeStates); + } + + for (Map.Entry<String, String> entry : snapshot.entrySet()) { + final String path = entry.getKey(); + final String state = entry.getValue(); + + if (state.equals(Environment.MEDIA_UNMOUNTED)) { + int rc = doMountVolume(path); + if (rc != StorageResultCode.OperationSucceeded) { + Slog.e(TAG, String.format("Boot-time mount failed (%d)", + rc)); + } + } else if (state.equals(Environment.MEDIA_SHARED)) { + /* + * Bootstrap UMS enabled state since vold indicates + * the volume is shared (runtime restart while ums enabled) + */ + notifyVolumeStateChange(null, path, VolumeState.NoMedia, + VolumeState.Shared); + } + } + + // Push mounted state for all emulated storage + synchronized (mVolumesLock) { + for (StorageVolume volume : mVolumes) { + if (volume.isEmulated()) { + updatePublicVolumeState(volume, Environment.MEDIA_MOUNTED); + } + } + } + + /* + * If UMS was connected on boot, send the connected event + * now that we're up. + */ + if (mSendUmsConnectedOnBoot) { + sendUmsIntent(true); + mSendUmsConnectedOnBoot = false; + } + } + + private final BroadcastReceiver mUserReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userId == -1) return; + final UserHandle user = new UserHandle(userId); + + final String action = intent.getAction(); + if (Intent.ACTION_USER_ADDED.equals(action)) { + synchronized (mVolumesLock) { + createEmulatedVolumeForUserLocked(user); + } + + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + synchronized (mVolumesLock) { + final List<StorageVolume> toRemove = Lists.newArrayList(); + for (StorageVolume volume : mVolumes) { + if (user.equals(volume.getOwner())) { + toRemove.add(volume); + } + } + for (StorageVolume volume : toRemove) { + removeVolumeLocked(volume); + } + } + } + } + }; + + private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + boolean available = (intent.getBooleanExtra(UsbManager.USB_CONNECTED, false) && + intent.getBooleanExtra(UsbManager.USB_FUNCTION_MASS_STORAGE, false)); + notifyShareAvailabilityChange(available); + } + }; + + private final BroadcastReceiver mIdleMaintenanceReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + waitForReady(); + String action = intent.getAction(); + // Since fstrim will be run on a daily basis we do not expect + // fstrim to be too long, so it is not interruptible. We will + // implement interruption only in case we see issues. + if (Intent.ACTION_IDLE_MAINTENANCE_START.equals(action)) { + try { + // This method runs on the handler thread, + // so it is safe to directly call into vold. + mConnector.execute("fstrim", "dotrim"); + EventLogTags.writeFstrimStart(SystemClock.elapsedRealtime()); + } catch (NativeDaemonConnectorException ndce) { + Slog.e(TAG, "Failed to run fstrim!"); + } + } + } + }; + + private final class MountServiceBinderListener implements IBinder.DeathRecipient { + final IMountServiceListener mListener; + + MountServiceBinderListener(IMountServiceListener listener) { + mListener = listener; + + } + + public void binderDied() { + if (LOCAL_LOGD) Slog.d(TAG, "An IMountServiceListener has died!"); + synchronized (mListeners) { + mListeners.remove(this); + mListener.asBinder().unlinkToDeath(this, 0); + } + } + } + + private void doShareUnshareVolume(String path, String method, boolean enable) { + // TODO: Add support for multiple share methods + if (!method.equals("ums")) { + throw new IllegalArgumentException(String.format("Method %s not supported", method)); + } + + try { + mConnector.execute("volume", enable ? "share" : "unshare", path, method); + } catch (NativeDaemonConnectorException e) { + Slog.e(TAG, "Failed to share/unshare", e); + } + } + + private void updatePublicVolumeState(StorageVolume volume, String state) { + final String path = volume.getPath(); + final String oldState; + synchronized (mVolumesLock) { + oldState = mVolumeStates.put(path, state); + volume.setState(state); + } + + if (state.equals(oldState)) { + Slog.w(TAG, String.format("Duplicate state transition (%s -> %s) for %s", + state, state, path)); + return; + } + + Slog.d(TAG, "volume state changed for " + path + " (" + oldState + " -> " + state + ")"); + + // Tell PackageManager about changes to primary volume state, but only + // when not emulated. + if (volume.isPrimary() && !volume.isEmulated()) { + if (Environment.MEDIA_UNMOUNTED.equals(state)) { + mPms.updateExternalMediaStatus(false, false); + + /* + * Some OBBs might have been unmounted when this volume was + * unmounted, so send a message to the handler to let it know to + * remove those from the list of mounted OBBS. + */ + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage( + OBB_FLUSH_MOUNT_STATE, path)); + } else if (Environment.MEDIA_MOUNTED.equals(state)) { + mPms.updateExternalMediaStatus(true, false); + } + } + + synchronized (mListeners) { + for (int i = mListeners.size() -1; i >= 0; i--) { + MountServiceBinderListener bl = mListeners.get(i); + try { + bl.mListener.onStorageStateChanged(path, oldState, state); + } catch (RemoteException rex) { + Slog.e(TAG, "Listener dead"); + mListeners.remove(i); + } catch (Exception ex) { + Slog.e(TAG, "Listener failed", ex); + } + } + } + } + + /** + * Callback from NativeDaemonConnector + */ + public void onDaemonConnected() { + /* + * Since we'll be calling back into the NativeDaemonConnector, + * we need to do our work in a new thread. + */ + new Thread("MountService#onDaemonConnected") { + @Override + public void run() { + /** + * Determine media state and UMS detection status + */ + try { + final String[] vols = NativeDaemonEvent.filterMessageList( + mConnector.executeForList("volume", "list"), + VoldResponseCode.VolumeListResult); + for (String volstr : vols) { + String[] tok = volstr.split(" "); + // FMT: <label> <mountpoint> <state> + String path = tok[1]; + String state = Environment.MEDIA_REMOVED; + + final StorageVolume volume; + synchronized (mVolumesLock) { + volume = mVolumesByPath.get(path); + } + + int st = Integer.parseInt(tok[2]); + if (st == VolumeState.NoMedia) { + state = Environment.MEDIA_REMOVED; + } else if (st == VolumeState.Idle) { + state = Environment.MEDIA_UNMOUNTED; + } else if (st == VolumeState.Mounted) { + state = Environment.MEDIA_MOUNTED; + Slog.i(TAG, "Media already mounted on daemon connection"); + } else if (st == VolumeState.Shared) { + state = Environment.MEDIA_SHARED; + Slog.i(TAG, "Media shared on daemon connection"); + } else { + throw new Exception(String.format("Unexpected state %d", st)); + } + + if (state != null) { + if (DEBUG_EVENTS) Slog.i(TAG, "Updating valid state " + state); + updatePublicVolumeState(volume, state); + } + } + } catch (Exception e) { + Slog.e(TAG, "Error processing initial volume state", e); + final StorageVolume primary = getPrimaryPhysicalVolume(); + if (primary != null) { + updatePublicVolumeState(primary, Environment.MEDIA_REMOVED); + } + } + + /* + * Now that we've done our initialization, release + * the hounds! + */ + mConnectedSignal.countDown(); + + // Let package manager load internal ASECs. + mPms.scanAvailableAsecs(); + + // Notify people waiting for ASECs to be scanned that it's done. + mAsecsScanned.countDown(); + } + }.start(); + } + + /** + * Callback from NativeDaemonConnector + */ + public boolean onEvent(int code, String raw, String[] cooked) { + if (DEBUG_EVENTS) { + StringBuilder builder = new StringBuilder(); + builder.append("onEvent::"); + builder.append(" raw= " + raw); + if (cooked != null) { + builder.append(" cooked = " ); + for (String str : cooked) { + builder.append(" " + str); + } + } + Slog.i(TAG, builder.toString()); + } + if (code == VoldResponseCode.VolumeStateChange) { + /* + * One of the volumes we're managing has changed state. + * Format: "NNN Volume <label> <path> state changed + * from <old_#> (<old_str>) to <new_#> (<new_str>)" + */ + notifyVolumeStateChange( + cooked[2], cooked[3], Integer.parseInt(cooked[7]), + Integer.parseInt(cooked[10])); + } else if (code == VoldResponseCode.VolumeUuidChange) { + // Format: nnn <label> <path> <uuid> + final String path = cooked[2]; + final String uuid = (cooked.length > 3) ? cooked[3] : null; + + final StorageVolume vol = mVolumesByPath.get(path); + if (vol != null) { + vol.setUuid(uuid); + } + + } else if (code == VoldResponseCode.VolumeUserLabelChange) { + // Format: nnn <label> <path> <label> + final String path = cooked[2]; + final String userLabel = (cooked.length > 3) ? cooked[3] : null; + + final StorageVolume vol = mVolumesByPath.get(path); + if (vol != null) { + vol.setUserLabel(userLabel); + } + + } else if ((code == VoldResponseCode.VolumeDiskInserted) || + (code == VoldResponseCode.VolumeDiskRemoved) || + (code == VoldResponseCode.VolumeBadRemoval)) { + // FMT: NNN Volume <label> <mountpoint> disk inserted (<major>:<minor>) + // FMT: NNN Volume <label> <mountpoint> disk removed (<major>:<minor>) + // FMT: NNN Volume <label> <mountpoint> bad removal (<major>:<minor>) + String action = null; + final String label = cooked[2]; + final String path = cooked[3]; + int major = -1; + int minor = -1; + + try { + String devComp = cooked[6].substring(1, cooked[6].length() -1); + String[] devTok = devComp.split(":"); + major = Integer.parseInt(devTok[0]); + minor = Integer.parseInt(devTok[1]); + } catch (Exception ex) { + Slog.e(TAG, "Failed to parse major/minor", ex); + } + + final StorageVolume volume; + final String state; + synchronized (mVolumesLock) { + volume = mVolumesByPath.get(path); + state = mVolumeStates.get(path); + } + + if (code == VoldResponseCode.VolumeDiskInserted) { + new Thread("MountService#VolumeDiskInserted") { + @Override + public void run() { + try { + int rc; + if ((rc = doMountVolume(path)) != StorageResultCode.OperationSucceeded) { + Slog.w(TAG, String.format("Insertion mount failed (%d)", rc)); + } + } catch (Exception ex) { + Slog.w(TAG, "Failed to mount media on insertion", ex); + } + } + }.start(); + } else if (code == VoldResponseCode.VolumeDiskRemoved) { + /* + * This event gets trumped if we're already in BAD_REMOVAL state + */ + if (getVolumeState(path).equals(Environment.MEDIA_BAD_REMOVAL)) { + return true; + } + /* Send the media unmounted event first */ + if (DEBUG_EVENTS) Slog.i(TAG, "Sending unmounted event first"); + updatePublicVolumeState(volume, Environment.MEDIA_UNMOUNTED); + sendStorageIntent(Environment.MEDIA_UNMOUNTED, volume, UserHandle.ALL); + + if (DEBUG_EVENTS) Slog.i(TAG, "Sending media removed"); + updatePublicVolumeState(volume, Environment.MEDIA_REMOVED); + action = Intent.ACTION_MEDIA_REMOVED; + } else if (code == VoldResponseCode.VolumeBadRemoval) { + if (DEBUG_EVENTS) Slog.i(TAG, "Sending unmounted event first"); + /* Send the media unmounted event first */ + updatePublicVolumeState(volume, Environment.MEDIA_UNMOUNTED); + sendStorageIntent(Intent.ACTION_MEDIA_UNMOUNTED, volume, UserHandle.ALL); + + if (DEBUG_EVENTS) Slog.i(TAG, "Sending media bad removal"); + updatePublicVolumeState(volume, Environment.MEDIA_BAD_REMOVAL); + action = Intent.ACTION_MEDIA_BAD_REMOVAL; + } else if (code == VoldResponseCode.FstrimCompleted) { + EventLogTags.writeFstrimFinish(SystemClock.elapsedRealtime()); + } else { + Slog.e(TAG, String.format("Unknown code {%d}", code)); + } + + if (action != null) { + sendStorageIntent(action, volume, UserHandle.ALL); + } + } else { + return false; + } + + return true; + } + + private void notifyVolumeStateChange(String label, String path, int oldState, int newState) { + final StorageVolume volume; + final String state; + synchronized (mVolumesLock) { + volume = mVolumesByPath.get(path); + state = getVolumeState(path); + } + + if (DEBUG_EVENTS) Slog.i(TAG, "notifyVolumeStateChange::" + state); + + String action = null; + + if (oldState == VolumeState.Shared && newState != oldState) { + if (LOCAL_LOGD) Slog.d(TAG, "Sending ACTION_MEDIA_UNSHARED intent"); + sendStorageIntent(Intent.ACTION_MEDIA_UNSHARED, volume, UserHandle.ALL); + } + + if (newState == VolumeState.Init) { + } else if (newState == VolumeState.NoMedia) { + // NoMedia is handled via Disk Remove events + } else if (newState == VolumeState.Idle) { + /* + * Don't notify if we're in BAD_REMOVAL, NOFS, UNMOUNTABLE, or + * if we're in the process of enabling UMS + */ + if (!state.equals( + Environment.MEDIA_BAD_REMOVAL) && !state.equals( + Environment.MEDIA_NOFS) && !state.equals( + Environment.MEDIA_UNMOUNTABLE) && !getUmsEnabling()) { + if (DEBUG_EVENTS) Slog.i(TAG, "updating volume state for media bad removal nofs and unmountable"); + updatePublicVolumeState(volume, Environment.MEDIA_UNMOUNTED); + action = Intent.ACTION_MEDIA_UNMOUNTED; + } + } else if (newState == VolumeState.Pending) { + } else if (newState == VolumeState.Checking) { + if (DEBUG_EVENTS) Slog.i(TAG, "updating volume state checking"); + updatePublicVolumeState(volume, Environment.MEDIA_CHECKING); + action = Intent.ACTION_MEDIA_CHECKING; + } else if (newState == VolumeState.Mounted) { + if (DEBUG_EVENTS) Slog.i(TAG, "updating volume state mounted"); + updatePublicVolumeState(volume, Environment.MEDIA_MOUNTED); + action = Intent.ACTION_MEDIA_MOUNTED; + } else if (newState == VolumeState.Unmounting) { + action = Intent.ACTION_MEDIA_EJECT; + } else if (newState == VolumeState.Formatting) { + } else if (newState == VolumeState.Shared) { + if (DEBUG_EVENTS) Slog.i(TAG, "Updating volume state media mounted"); + /* Send the media unmounted event first */ + updatePublicVolumeState(volume, Environment.MEDIA_UNMOUNTED); + sendStorageIntent(Intent.ACTION_MEDIA_UNMOUNTED, volume, UserHandle.ALL); + + if (DEBUG_EVENTS) Slog.i(TAG, "Updating media shared"); + updatePublicVolumeState(volume, Environment.MEDIA_SHARED); + action = Intent.ACTION_MEDIA_SHARED; + if (LOCAL_LOGD) Slog.d(TAG, "Sending ACTION_MEDIA_SHARED intent"); + } else if (newState == VolumeState.SharedMnt) { + Slog.e(TAG, "Live shared mounts not supported yet!"); + return; + } else { + Slog.e(TAG, "Unhandled VolumeState {" + newState + "}"); + } + + if (action != null) { + sendStorageIntent(action, volume, UserHandle.ALL); + } + } + + private int doMountVolume(String path) { + int rc = StorageResultCode.OperationSucceeded; + + final StorageVolume volume; + synchronized (mVolumesLock) { + volume = mVolumesByPath.get(path); + } + + if (DEBUG_EVENTS) Slog.i(TAG, "doMountVolume: Mouting " + path); + try { + mConnector.execute("volume", "mount", path); + } catch (NativeDaemonConnectorException e) { + /* + * Mount failed for some reason + */ + String action = null; + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedNoMedia) { + /* + * Attempt to mount but no media inserted + */ + rc = StorageResultCode.OperationFailedNoMedia; + } else if (code == VoldResponseCode.OpFailedMediaBlank) { + if (DEBUG_EVENTS) Slog.i(TAG, " updating volume state :: media nofs"); + /* + * Media is blank or does not contain a supported filesystem + */ + updatePublicVolumeState(volume, Environment.MEDIA_NOFS); + action = Intent.ACTION_MEDIA_NOFS; + rc = StorageResultCode.OperationFailedMediaBlank; + } else if (code == VoldResponseCode.OpFailedMediaCorrupt) { + if (DEBUG_EVENTS) Slog.i(TAG, "updating volume state media corrupt"); + /* + * Volume consistency check failed + */ + updatePublicVolumeState(volume, Environment.MEDIA_UNMOUNTABLE); + action = Intent.ACTION_MEDIA_UNMOUNTABLE; + rc = StorageResultCode.OperationFailedMediaCorrupt; + } else { + rc = StorageResultCode.OperationFailedInternalError; + } + + /* + * Send broadcast intent (if required for the failure) + */ + if (action != null) { + sendStorageIntent(action, volume, UserHandle.ALL); + } + } + + return rc; + } + + /* + * If force is not set, we do not unmount if there are + * processes holding references to the volume about to be unmounted. + * If force is set, all the processes holding references need to be + * killed via the ActivityManager before actually unmounting the volume. + * This might even take a while and might be retried after timed delays + * to make sure we dont end up in an instable state and kill some core + * processes. + * If removeEncryption is set, force is implied, and the system will remove any encryption + * mapping set on the volume when unmounting. + */ + private int doUnmountVolume(String path, boolean force, boolean removeEncryption) { + if (!getVolumeState(path).equals(Environment.MEDIA_MOUNTED)) { + return VoldResponseCode.OpFailedVolNotMounted; + } + + /* + * Force a GC to make sure AssetManagers in other threads of the + * system_server are cleaned up. We have to do this since AssetManager + * instances are kept as a WeakReference and it's possible we have files + * open on the external storage. + */ + Runtime.getRuntime().gc(); + + // Redundant probably. But no harm in updating state again. + mPms.updateExternalMediaStatus(false, false); + try { + final Command cmd = new Command("volume", "unmount", path); + if (removeEncryption) { + cmd.appendArg("force_and_revert"); + } else if (force) { + cmd.appendArg("force"); + } + mConnector.execute(cmd); + // We unmounted the volume. None of the asec containers are available now. + synchronized (mAsecMountSet) { + mAsecMountSet.clear(); + } + return StorageResultCode.OperationSucceeded; + } catch (NativeDaemonConnectorException e) { + // Don't worry about mismatch in PackageManager since the + // call back will handle the status changes any way. + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedVolNotMounted) { + return StorageResultCode.OperationFailedStorageNotMounted; + } else if (code == VoldResponseCode.OpFailedStorageBusy) { + return StorageResultCode.OperationFailedStorageBusy; + } else { + return StorageResultCode.OperationFailedInternalError; + } + } + } + + private int doFormatVolume(String path) { + try { + mConnector.execute("volume", "format", path); + return StorageResultCode.OperationSucceeded; + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedNoMedia) { + return StorageResultCode.OperationFailedNoMedia; + } else if (code == VoldResponseCode.OpFailedMediaCorrupt) { + return StorageResultCode.OperationFailedMediaCorrupt; + } else { + return StorageResultCode.OperationFailedInternalError; + } + } + } + + private boolean doGetVolumeShared(String path, String method) { + final NativeDaemonEvent event; + try { + event = mConnector.execute("volume", "shared", path, method); + } catch (NativeDaemonConnectorException ex) { + Slog.e(TAG, "Failed to read response to volume shared " + path + " " + method); + return false; + } + + if (event.getCode() == VoldResponseCode.ShareEnabledResult) { + return event.getMessage().endsWith("enabled"); + } else { + return false; + } + } + + private void notifyShareAvailabilityChange(final boolean avail) { + synchronized (mListeners) { + mUmsAvailable = avail; + for (int i = mListeners.size() -1; i >= 0; i--) { + MountServiceBinderListener bl = mListeners.get(i); + try { + bl.mListener.onUsbMassStorageConnectionChanged(avail); + } catch (RemoteException rex) { + Slog.e(TAG, "Listener dead"); + mListeners.remove(i); + } catch (Exception ex) { + Slog.e(TAG, "Listener failed", ex); + } + } + } + + if (mSystemReady == true) { + sendUmsIntent(avail); + } else { + mSendUmsConnectedOnBoot = avail; + } + + final StorageVolume primary = getPrimaryPhysicalVolume(); + if (avail == false && primary != null + && Environment.MEDIA_SHARED.equals(getVolumeState(primary.getPath()))) { + final String path = primary.getPath(); + /* + * USB mass storage disconnected while enabled + */ + new Thread("MountService#AvailabilityChange") { + @Override + public void run() { + try { + int rc; + Slog.w(TAG, "Disabling UMS after cable disconnect"); + doShareUnshareVolume(path, "ums", false); + if ((rc = doMountVolume(path)) != StorageResultCode.OperationSucceeded) { + Slog.e(TAG, String.format( + "Failed to remount {%s} on UMS enabled-disconnect (%d)", + path, rc)); + } + } catch (Exception ex) { + Slog.w(TAG, "Failed to mount media on UMS enabled-disconnect", ex); + } + } + }.start(); + } + } + + private void sendStorageIntent(String action, StorageVolume volume, UserHandle user) { + final Intent intent = new Intent(action, Uri.parse("file://" + volume.getPath())); + intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, volume); + Slog.d(TAG, "sendStorageIntent " + intent + " to " + user); + mContext.sendBroadcastAsUser(intent, user); + } + + private void sendUmsIntent(boolean c) { + mContext.sendBroadcastAsUser( + new Intent((c ? Intent.ACTION_UMS_CONNECTED : Intent.ACTION_UMS_DISCONNECTED)), + UserHandle.ALL); + } + + private void validatePermission(String perm) { + if (mContext.checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException(String.format("Requires %s permission", perm)); + } + } + + // Storage list XML tags + private static final String TAG_STORAGE_LIST = "StorageList"; + private static final String TAG_STORAGE = "storage"; + + private void readStorageListLocked() { + mVolumes.clear(); + mVolumeStates.clear(); + + Resources resources = mContext.getResources(); + + int id = com.android.internal.R.xml.storage_list; + XmlResourceParser parser = resources.getXml(id); + AttributeSet attrs = Xml.asAttributeSet(parser); + + try { + XmlUtils.beginDocument(parser, TAG_STORAGE_LIST); + while (true) { + XmlUtils.nextElement(parser); + + String element = parser.getName(); + if (element == null) break; + + if (TAG_STORAGE.equals(element)) { + TypedArray a = resources.obtainAttributes(attrs, + com.android.internal.R.styleable.Storage); + + String path = a.getString( + com.android.internal.R.styleable.Storage_mountPoint); + int descriptionId = a.getResourceId( + com.android.internal.R.styleable.Storage_storageDescription, -1); + CharSequence description = a.getText( + com.android.internal.R.styleable.Storage_storageDescription); + boolean primary = a.getBoolean( + com.android.internal.R.styleable.Storage_primary, false); + boolean removable = a.getBoolean( + com.android.internal.R.styleable.Storage_removable, false); + boolean emulated = a.getBoolean( + com.android.internal.R.styleable.Storage_emulated, false); + int mtpReserve = a.getInt( + com.android.internal.R.styleable.Storage_mtpReserve, 0); + boolean allowMassStorage = a.getBoolean( + com.android.internal.R.styleable.Storage_allowMassStorage, false); + // resource parser does not support longs, so XML value is in megabytes + long maxFileSize = a.getInt( + com.android.internal.R.styleable.Storage_maxFileSize, 0) * 1024L * 1024L; + + Slog.d(TAG, "got storage path: " + path + " description: " + description + + " primary: " + primary + " removable: " + removable + + " emulated: " + emulated + " mtpReserve: " + mtpReserve + + " allowMassStorage: " + allowMassStorage + + " maxFileSize: " + maxFileSize); + + if (emulated) { + // For devices with emulated storage, we create separate + // volumes for each known user. + mEmulatedTemplate = new StorageVolume(null, descriptionId, true, false, + true, mtpReserve, false, maxFileSize, null); + + final UserManagerService userManager = UserManagerService.getInstance(); + for (UserInfo user : userManager.getUsers(false)) { + createEmulatedVolumeForUserLocked(user.getUserHandle()); + } + + } else { + if (path == null || description == null) { + Slog.e(TAG, "Missing storage path or description in readStorageList"); + } else { + final StorageVolume volume = new StorageVolume(new File(path), + descriptionId, primary, removable, emulated, mtpReserve, + allowMassStorage, maxFileSize, null); + addVolumeLocked(volume); + + // Until we hear otherwise, treat as unmounted + mVolumeStates.put(volume.getPath(), Environment.MEDIA_UNMOUNTED); + volume.setState(Environment.MEDIA_UNMOUNTED); + } + } + + a.recycle(); + } + } + } catch (XmlPullParserException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + // Compute storage ID for each physical volume; emulated storage is + // always 0 when defined. + int index = isExternalStorageEmulated() ? 1 : 0; + for (StorageVolume volume : mVolumes) { + if (!volume.isEmulated()) { + volume.setStorageId(index++); + } + } + parser.close(); + } + } + + /** + * Create and add new {@link StorageVolume} for given {@link UserHandle} + * using {@link #mEmulatedTemplate} as template. + */ + private void createEmulatedVolumeForUserLocked(UserHandle user) { + if (mEmulatedTemplate == null) { + throw new IllegalStateException("Missing emulated volume multi-user template"); + } + + final UserEnvironment userEnv = new UserEnvironment(user.getIdentifier()); + final File path = userEnv.getExternalStorageDirectory(); + final StorageVolume volume = StorageVolume.fromTemplate(mEmulatedTemplate, path, user); + volume.setStorageId(0); + addVolumeLocked(volume); + + if (mSystemReady) { + updatePublicVolumeState(volume, Environment.MEDIA_MOUNTED); + } else { + // Place stub status for early callers to find + mVolumeStates.put(volume.getPath(), Environment.MEDIA_MOUNTED); + volume.setState(Environment.MEDIA_MOUNTED); + } + } + + private void addVolumeLocked(StorageVolume volume) { + Slog.d(TAG, "addVolumeLocked() " + volume); + mVolumes.add(volume); + final StorageVolume existing = mVolumesByPath.put(volume.getPath(), volume); + if (existing != null) { + throw new IllegalStateException( + "Volume at " + volume.getPath() + " already exists: " + existing); + } + } + + private void removeVolumeLocked(StorageVolume volume) { + Slog.d(TAG, "removeVolumeLocked() " + volume); + mVolumes.remove(volume); + mVolumesByPath.remove(volume.getPath()); + mVolumeStates.remove(volume.getPath()); + } + + private StorageVolume getPrimaryPhysicalVolume() { + synchronized (mVolumesLock) { + for (StorageVolume volume : mVolumes) { + if (volume.isPrimary() && !volume.isEmulated()) { + return volume; + } + } + } + return null; + } + + /** + * Constructs a new MountService instance + * + * @param context Binder context for this service + */ + public MountService(Context context) { + mContext = context; + + synchronized (mVolumesLock) { + readStorageListLocked(); + } + + // XXX: This will go away soon in favor of IMountServiceObserver + mPms = (PackageManagerService) ServiceManager.getService("package"); + + HandlerThread hthread = new HandlerThread(TAG); + hthread.start(); + mHandler = new MountServiceHandler(hthread.getLooper()); + + // Watch for user changes + final IntentFilter userFilter = new IntentFilter(); + userFilter.addAction(Intent.ACTION_USER_ADDED); + userFilter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler); + + // Watch for USB changes on primary volume + final StorageVolume primary = getPrimaryPhysicalVolume(); + if (primary != null && primary.allowMassStorage()) { + mContext.registerReceiver( + mUsbReceiver, new IntentFilter(UsbManager.ACTION_USB_STATE), null, mHandler); + } + + // Watch for idle maintenance changes + IntentFilter idleMaintenanceFilter = new IntentFilter(); + idleMaintenanceFilter.addAction(Intent.ACTION_IDLE_MAINTENANCE_START); + mContext.registerReceiverAsUser(mIdleMaintenanceReceiver, UserHandle.ALL, + idleMaintenanceFilter, null, mHandler); + + // Add OBB Action Handler to MountService thread. + mObbActionHandler = new ObbActionHandler(IoThread.get().getLooper()); + + /* + * Create the connection to vold with a maximum queue of twice the + * amount of containers we'd ever expect to have. This keeps an + * "asec list" from blocking a thread repeatedly. + */ + mConnector = new NativeDaemonConnector(this, "vold", MAX_CONTAINERS * 2, VOLD_TAG, 25); + + Thread thread = new Thread(mConnector, VOLD_TAG); + thread.start(); + + // Add ourself to the Watchdog monitors if enabled. + if (WATCHDOG_ENABLE) { + Watchdog.getInstance().addMonitor(this); + } + } + + public void systemReady() { + mSystemReady = true; + mHandler.obtainMessage(H_SYSTEM_READY).sendToTarget(); + } + + /** + * Exposed API calls below here + */ + + public void registerListener(IMountServiceListener listener) { + synchronized (mListeners) { + MountServiceBinderListener bl = new MountServiceBinderListener(listener); + try { + listener.asBinder().linkToDeath(bl, 0); + mListeners.add(bl); + } catch (RemoteException rex) { + Slog.e(TAG, "Failed to link to listener death"); + } + } + } + + public void unregisterListener(IMountServiceListener listener) { + synchronized (mListeners) { + for(MountServiceBinderListener bl : mListeners) { + if (bl.mListener == listener) { + mListeners.remove(mListeners.indexOf(bl)); + listener.asBinder().unlinkToDeath(bl, 0); + return; + } + } + } + } + + public void shutdown(final IMountShutdownObserver observer) { + validatePermission(android.Manifest.permission.SHUTDOWN); + + Slog.i(TAG, "Shutting down"); + synchronized (mVolumesLock) { + for (String path : mVolumeStates.keySet()) { + String state = mVolumeStates.get(path); + + if (state.equals(Environment.MEDIA_SHARED)) { + /* + * If the media is currently shared, unshare it. + * XXX: This is still dangerous!. We should not + * be rebooting at *all* if UMS is enabled, since + * the UMS host could have dirty FAT cache entries + * yet to flush. + */ + setUsbMassStorageEnabled(false); + } else if (state.equals(Environment.MEDIA_CHECKING)) { + /* + * If the media is being checked, then we need to wait for + * it to complete before being able to proceed. + */ + // XXX: @hackbod - Should we disable the ANR timer here? + int retries = 30; + while (state.equals(Environment.MEDIA_CHECKING) && (retries-- >=0)) { + try { + Thread.sleep(1000); + } catch (InterruptedException iex) { + Slog.e(TAG, "Interrupted while waiting for media", iex); + break; + } + state = Environment.getExternalStorageState(); + } + if (retries == 0) { + Slog.e(TAG, "Timed out waiting for media to check"); + } + } + + if (state.equals(Environment.MEDIA_MOUNTED)) { + // Post a unmount message. + ShutdownCallBack ucb = new ShutdownCallBack(path, observer); + mHandler.sendMessage(mHandler.obtainMessage(H_UNMOUNT_PM_UPDATE, ucb)); + } else if (observer != null) { + /* + * Observer is waiting for onShutDownComplete when we are done. + * Since nothing will be done send notification directly so shutdown + * sequence can continue. + */ + try { + observer.onShutDownComplete(StorageResultCode.OperationSucceeded); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException when shutting down"); + } + } + } + } + } + + private boolean getUmsEnabling() { + synchronized (mListeners) { + return mUmsEnabling; + } + } + + private void setUmsEnabling(boolean enable) { + synchronized (mListeners) { + mUmsEnabling = enable; + } + } + + public boolean isUsbMassStorageConnected() { + waitForReady(); + + if (getUmsEnabling()) { + return true; + } + synchronized (mListeners) { + return mUmsAvailable; + } + } + + public void setUsbMassStorageEnabled(boolean enable) { + waitForReady(); + validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); + + final StorageVolume primary = getPrimaryPhysicalVolume(); + if (primary == null) return; + + // TODO: Add support for multiple share methods + + /* + * If the volume is mounted and we're enabling then unmount it + */ + String path = primary.getPath(); + String vs = getVolumeState(path); + String method = "ums"; + if (enable && vs.equals(Environment.MEDIA_MOUNTED)) { + // Override for isUsbMassStorageEnabled() + setUmsEnabling(enable); + UmsEnableCallBack umscb = new UmsEnableCallBack(path, method, true); + mHandler.sendMessage(mHandler.obtainMessage(H_UNMOUNT_PM_UPDATE, umscb)); + // Clear override + setUmsEnabling(false); + } + /* + * If we disabled UMS then mount the volume + */ + if (!enable) { + doShareUnshareVolume(path, method, enable); + if (doMountVolume(path) != StorageResultCode.OperationSucceeded) { + Slog.e(TAG, "Failed to remount " + path + + " after disabling share method " + method); + /* + * Even though the mount failed, the unshare didn't so don't indicate an error. + * The mountVolume() call will have set the storage state and sent the necessary + * broadcasts. + */ + } + } + } + + public boolean isUsbMassStorageEnabled() { + waitForReady(); + + final StorageVolume primary = getPrimaryPhysicalVolume(); + if (primary != null) { + return doGetVolumeShared(primary.getPath(), "ums"); + } else { + return false; + } + } + + /** + * @return state of the volume at the specified mount point + */ + public String getVolumeState(String mountPoint) { + synchronized (mVolumesLock) { + String state = mVolumeStates.get(mountPoint); + if (state == null) { + Slog.w(TAG, "getVolumeState(" + mountPoint + "): Unknown volume"); + if (SystemProperties.get("vold.encrypt_progress").length() != 0) { + state = Environment.MEDIA_REMOVED; + } else { + throw new IllegalArgumentException(); + } + } + + return state; + } + } + + @Override + public boolean isExternalStorageEmulated() { + return mEmulatedTemplate != null; + } + + public int mountVolume(String path) { + validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); + + waitForReady(); + return doMountVolume(path); + } + + public void unmountVolume(String path, boolean force, boolean removeEncryption) { + validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); + waitForReady(); + + String volState = getVolumeState(path); + if (DEBUG_UNMOUNT) { + Slog.i(TAG, "Unmounting " + path + + " force = " + force + + " removeEncryption = " + removeEncryption); + } + if (Environment.MEDIA_UNMOUNTED.equals(volState) || + Environment.MEDIA_REMOVED.equals(volState) || + Environment.MEDIA_SHARED.equals(volState) || + Environment.MEDIA_UNMOUNTABLE.equals(volState)) { + // Media already unmounted or cannot be unmounted. + // TODO return valid return code when adding observer call back. + return; + } + UnmountCallBack ucb = new UnmountCallBack(path, force, removeEncryption); + mHandler.sendMessage(mHandler.obtainMessage(H_UNMOUNT_PM_UPDATE, ucb)); + } + + public int formatVolume(String path) { + validatePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS); + waitForReady(); + + return doFormatVolume(path); + } + + public int[] getStorageUsers(String path) { + validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); + waitForReady(); + try { + final String[] r = NativeDaemonEvent.filterMessageList( + mConnector.executeForList("storage", "users", path), + VoldResponseCode.StorageUsersListResult); + + // FMT: <pid> <process name> + int[] data = new int[r.length]; + for (int i = 0; i < r.length; i++) { + String[] tok = r[i].split(" "); + try { + data[i] = Integer.parseInt(tok[0]); + } catch (NumberFormatException nfe) { + Slog.e(TAG, String.format("Error parsing pid %s", tok[0])); + return new int[0]; + } + } + return data; + } catch (NativeDaemonConnectorException e) { + Slog.e(TAG, "Failed to retrieve storage users list", e); + return new int[0]; + } + } + + private void warnOnNotMounted() { + final StorageVolume primary = getPrimaryPhysicalVolume(); + if (primary != null) { + boolean mounted = false; + try { + mounted = Environment.MEDIA_MOUNTED.equals(getVolumeState(primary.getPath())); + } catch (IllegalArgumentException e) { + } + + if (!mounted) { + Slog.w(TAG, "getSecureContainerList() called when storage not mounted"); + } + } + } + + public String[] getSecureContainerList() { + validatePermission(android.Manifest.permission.ASEC_ACCESS); + waitForReady(); + warnOnNotMounted(); + + try { + return NativeDaemonEvent.filterMessageList( + mConnector.executeForList("asec", "list"), VoldResponseCode.AsecListResult); + } catch (NativeDaemonConnectorException e) { + return new String[0]; + } + } + + public int createSecureContainer(String id, int sizeMb, String fstype, String key, + int ownerUid, boolean external) { + validatePermission(android.Manifest.permission.ASEC_CREATE); + waitForReady(); + warnOnNotMounted(); + + int rc = StorageResultCode.OperationSucceeded; + try { + mConnector.execute("asec", "create", id, sizeMb, fstype, new SensitiveArg(key), + ownerUid, external ? "1" : "0"); + } catch (NativeDaemonConnectorException e) { + rc = StorageResultCode.OperationFailedInternalError; + } + + if (rc == StorageResultCode.OperationSucceeded) { + synchronized (mAsecMountSet) { + mAsecMountSet.add(id); + } + } + return rc; + } + + public int finalizeSecureContainer(String id) { + validatePermission(android.Manifest.permission.ASEC_CREATE); + warnOnNotMounted(); + + int rc = StorageResultCode.OperationSucceeded; + try { + mConnector.execute("asec", "finalize", id); + /* + * Finalization does a remount, so no need + * to update mAsecMountSet + */ + } catch (NativeDaemonConnectorException e) { + rc = StorageResultCode.OperationFailedInternalError; + } + return rc; + } + + public int fixPermissionsSecureContainer(String id, int gid, String filename) { + validatePermission(android.Manifest.permission.ASEC_CREATE); + warnOnNotMounted(); + + int rc = StorageResultCode.OperationSucceeded; + try { + mConnector.execute("asec", "fixperms", id, gid, filename); + /* + * Fix permissions does a remount, so no need to update + * mAsecMountSet + */ + } catch (NativeDaemonConnectorException e) { + rc = StorageResultCode.OperationFailedInternalError; + } + return rc; + } + + public int destroySecureContainer(String id, boolean force) { + validatePermission(android.Manifest.permission.ASEC_DESTROY); + waitForReady(); + warnOnNotMounted(); + + /* + * Force a GC to make sure AssetManagers in other threads of the + * system_server are cleaned up. We have to do this since AssetManager + * instances are kept as a WeakReference and it's possible we have files + * open on the external storage. + */ + Runtime.getRuntime().gc(); + + int rc = StorageResultCode.OperationSucceeded; + try { + final Command cmd = new Command("asec", "destroy", id); + if (force) { + cmd.appendArg("force"); + } + mConnector.execute(cmd); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedStorageBusy) { + rc = StorageResultCode.OperationFailedStorageBusy; + } else { + rc = StorageResultCode.OperationFailedInternalError; + } + } + + if (rc == StorageResultCode.OperationSucceeded) { + synchronized (mAsecMountSet) { + if (mAsecMountSet.contains(id)) { + mAsecMountSet.remove(id); + } + } + } + + return rc; + } + + public int mountSecureContainer(String id, String key, int ownerUid) { + validatePermission(android.Manifest.permission.ASEC_MOUNT_UNMOUNT); + waitForReady(); + warnOnNotMounted(); + + synchronized (mAsecMountSet) { + if (mAsecMountSet.contains(id)) { + return StorageResultCode.OperationFailedStorageMounted; + } + } + + int rc = StorageResultCode.OperationSucceeded; + try { + mConnector.execute("asec", "mount", id, new SensitiveArg(key), ownerUid); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code != VoldResponseCode.OpFailedStorageBusy) { + rc = StorageResultCode.OperationFailedInternalError; + } + } + + if (rc == StorageResultCode.OperationSucceeded) { + synchronized (mAsecMountSet) { + mAsecMountSet.add(id); + } + } + return rc; + } + + public int unmountSecureContainer(String id, boolean force) { + validatePermission(android.Manifest.permission.ASEC_MOUNT_UNMOUNT); + waitForReady(); + warnOnNotMounted(); + + synchronized (mAsecMountSet) { + if (!mAsecMountSet.contains(id)) { + return StorageResultCode.OperationFailedStorageNotMounted; + } + } + + /* + * Force a GC to make sure AssetManagers in other threads of the + * system_server are cleaned up. We have to do this since AssetManager + * instances are kept as a WeakReference and it's possible we have files + * open on the external storage. + */ + Runtime.getRuntime().gc(); + + int rc = StorageResultCode.OperationSucceeded; + try { + final Command cmd = new Command("asec", "unmount", id); + if (force) { + cmd.appendArg("force"); + } + mConnector.execute(cmd); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedStorageBusy) { + rc = StorageResultCode.OperationFailedStorageBusy; + } else { + rc = StorageResultCode.OperationFailedInternalError; + } + } + + if (rc == StorageResultCode.OperationSucceeded) { + synchronized (mAsecMountSet) { + mAsecMountSet.remove(id); + } + } + return rc; + } + + public boolean isSecureContainerMounted(String id) { + validatePermission(android.Manifest.permission.ASEC_ACCESS); + waitForReady(); + warnOnNotMounted(); + + synchronized (mAsecMountSet) { + return mAsecMountSet.contains(id); + } + } + + public int renameSecureContainer(String oldId, String newId) { + validatePermission(android.Manifest.permission.ASEC_RENAME); + waitForReady(); + warnOnNotMounted(); + + synchronized (mAsecMountSet) { + /* + * Because a mounted container has active internal state which cannot be + * changed while active, we must ensure both ids are not currently mounted. + */ + if (mAsecMountSet.contains(oldId) || mAsecMountSet.contains(newId)) { + return StorageResultCode.OperationFailedStorageMounted; + } + } + + int rc = StorageResultCode.OperationSucceeded; + try { + mConnector.execute("asec", "rename", oldId, newId); + } catch (NativeDaemonConnectorException e) { + rc = StorageResultCode.OperationFailedInternalError; + } + + return rc; + } + + public String getSecureContainerPath(String id) { + validatePermission(android.Manifest.permission.ASEC_ACCESS); + waitForReady(); + warnOnNotMounted(); + + final NativeDaemonEvent event; + try { + event = mConnector.execute("asec", "path", id); + event.checkCode(VoldResponseCode.AsecPathResult); + return event.getMessage(); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedStorageNotFound) { + Slog.i(TAG, String.format("Container '%s' not found", id)); + return null; + } else { + throw new IllegalStateException(String.format("Unexpected response code %d", code)); + } + } + } + + public String getSecureContainerFilesystemPath(String id) { + validatePermission(android.Manifest.permission.ASEC_ACCESS); + waitForReady(); + warnOnNotMounted(); + + final NativeDaemonEvent event; + try { + event = mConnector.execute("asec", "fspath", id); + event.checkCode(VoldResponseCode.AsecPathResult); + return event.getMessage(); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedStorageNotFound) { + Slog.i(TAG, String.format("Container '%s' not found", id)); + return null; + } else { + throw new IllegalStateException(String.format("Unexpected response code %d", code)); + } + } + } + + public void finishMediaUpdate() { + mHandler.sendEmptyMessage(H_UNMOUNT_PM_DONE); + } + + private boolean isUidOwnerOfPackageOrSystem(String packageName, int callerUid) { + if (callerUid == android.os.Process.SYSTEM_UID) { + return true; + } + + if (packageName == null) { + return false; + } + + final int packageUid = mPms.getPackageUid(packageName, UserHandle.getUserId(callerUid)); + + if (DEBUG_OBB) { + Slog.d(TAG, "packageName = " + packageName + ", packageUid = " + + packageUid + ", callerUid = " + callerUid); + } + + return callerUid == packageUid; + } + + public String getMountedObbPath(String rawPath) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + + waitForReady(); + warnOnNotMounted(); + + final ObbState state; + synchronized (mObbPathToStateMap) { + state = mObbPathToStateMap.get(rawPath); + } + if (state == null) { + Slog.w(TAG, "Failed to find OBB mounted at " + rawPath); + return null; + } + + final NativeDaemonEvent event; + try { + event = mConnector.execute("obb", "path", state.voldPath); + event.checkCode(VoldResponseCode.AsecPathResult); + return event.getMessage(); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedStorageNotFound) { + return null; + } else { + throw new IllegalStateException(String.format("Unexpected response code %d", code)); + } + } + } + + @Override + public boolean isObbMounted(String rawPath) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + synchronized (mObbMounts) { + return mObbPathToStateMap.containsKey(rawPath); + } + } + + @Override + public void mountObb( + String rawPath, String canonicalPath, String key, IObbActionListener token, int nonce) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + Preconditions.checkNotNull(canonicalPath, "canonicalPath cannot be null"); + Preconditions.checkNotNull(token, "token cannot be null"); + + final int callingUid = Binder.getCallingUid(); + final ObbState obbState = new ObbState(rawPath, canonicalPath, callingUid, token, nonce); + final ObbAction action = new MountObbAction(obbState, key, callingUid); + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_RUN_ACTION, action)); + + if (DEBUG_OBB) + Slog.i(TAG, "Send to OBB handler: " + action.toString()); + } + + @Override + public void unmountObb(String rawPath, boolean force, IObbActionListener token, int nonce) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + + final ObbState existingState; + synchronized (mObbPathToStateMap) { + existingState = mObbPathToStateMap.get(rawPath); + } + + if (existingState != null) { + // TODO: separate state object from request data + final int callingUid = Binder.getCallingUid(); + final ObbState newState = new ObbState( + rawPath, existingState.canonicalPath, callingUid, token, nonce); + final ObbAction action = new UnmountObbAction(newState, force); + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_RUN_ACTION, action)); + + if (DEBUG_OBB) + Slog.i(TAG, "Send to OBB handler: " + action.toString()); + } else { + Slog.w(TAG, "Unknown OBB mount at " + rawPath); + } + } + + @Override + public int getEncryptionState() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.CRYPT_KEEPER, + "no permission to access the crypt keeper"); + + waitForReady(); + + final NativeDaemonEvent event; + try { + event = mConnector.execute("cryptfs", "cryptocomplete"); + return Integer.parseInt(event.getMessage()); + } catch (NumberFormatException e) { + // Bad result - unexpected. + Slog.w(TAG, "Unable to parse result from cryptfs cryptocomplete"); + return ENCRYPTION_STATE_ERROR_UNKNOWN; + } catch (NativeDaemonConnectorException e) { + // Something bad happened. + Slog.w(TAG, "Error in communicating with cryptfs in validating"); + return ENCRYPTION_STATE_ERROR_UNKNOWN; + } + } + + @Override + public int decryptStorage(String password) { + if (TextUtils.isEmpty(password)) { + throw new IllegalArgumentException("password cannot be empty"); + } + + mContext.enforceCallingOrSelfPermission(Manifest.permission.CRYPT_KEEPER, + "no permission to access the crypt keeper"); + + waitForReady(); + + if (DEBUG_EVENTS) { + Slog.i(TAG, "decrypting storage..."); + } + + final NativeDaemonEvent event; + try { + event = mConnector.execute("cryptfs", "checkpw", new SensitiveArg(password)); + + final int code = Integer.parseInt(event.getMessage()); + if (code == 0) { + // Decrypt was successful. Post a delayed message before restarting in order + // to let the UI to clear itself + mHandler.postDelayed(new Runnable() { + public void run() { + try { + mConnector.execute("cryptfs", "restart"); + } catch (NativeDaemonConnectorException e) { + Slog.e(TAG, "problem executing in background", e); + } + } + }, 1000); // 1 second + } + + return code; + } catch (NativeDaemonConnectorException e) { + // Decryption failed + return e.getCode(); + } + } + + public int encryptStorage(String password) { + if (TextUtils.isEmpty(password)) { + throw new IllegalArgumentException("password cannot be empty"); + } + + mContext.enforceCallingOrSelfPermission(Manifest.permission.CRYPT_KEEPER, + "no permission to access the crypt keeper"); + + waitForReady(); + + if (DEBUG_EVENTS) { + Slog.i(TAG, "encrypting storage..."); + } + + try { + mConnector.execute("cryptfs", "enablecrypto", "inplace", new SensitiveArg(password)); + } catch (NativeDaemonConnectorException e) { + // Encryption failed + return e.getCode(); + } + + return 0; + } + + public int changeEncryptionPassword(String password) { + if (TextUtils.isEmpty(password)) { + throw new IllegalArgumentException("password cannot be empty"); + } + + mContext.enforceCallingOrSelfPermission(Manifest.permission.CRYPT_KEEPER, + "no permission to access the crypt keeper"); + + waitForReady(); + + if (DEBUG_EVENTS) { + Slog.i(TAG, "changing encryption password..."); + } + + final NativeDaemonEvent event; + try { + event = mConnector.execute("cryptfs", "changepw", new SensitiveArg(password)); + return Integer.parseInt(event.getMessage()); + } catch (NativeDaemonConnectorException e) { + // Encryption failed + return e.getCode(); + } + } + + /** + * Validate a user-supplied password string with cryptfs + */ + @Override + public int verifyEncryptionPassword(String password) throws RemoteException { + // Only the system process is permitted to validate passwords + if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) { + throw new SecurityException("no permission to access the crypt keeper"); + } + + mContext.enforceCallingOrSelfPermission(Manifest.permission.CRYPT_KEEPER, + "no permission to access the crypt keeper"); + + if (TextUtils.isEmpty(password)) { + throw new IllegalArgumentException("password cannot be empty"); + } + + waitForReady(); + + if (DEBUG_EVENTS) { + Slog.i(TAG, "validating encryption password..."); + } + + final NativeDaemonEvent event; + try { + event = mConnector.execute("cryptfs", "verifypw", new SensitiveArg(password)); + Slog.i(TAG, "cryptfs verifypw => " + event.getMessage()); + return Integer.parseInt(event.getMessage()); + } catch (NativeDaemonConnectorException e) { + // Encryption failed + return e.getCode(); + } + } + + @Override + public int mkdirs(String callingPkg, String appPath) { + final int userId = UserHandle.getUserId(Binder.getCallingUid()); + final UserEnvironment userEnv = new UserEnvironment(userId); + + // Validate that reported package name belongs to caller + final AppOpsManager appOps = (AppOpsManager) mContext.getSystemService( + Context.APP_OPS_SERVICE); + appOps.checkPackage(Binder.getCallingUid(), callingPkg); + + try { + appPath = new File(appPath).getCanonicalPath(); + } catch (IOException e) { + Slog.e(TAG, "Failed to resolve " + appPath + ": " + e); + return -1; + } + + if (!appPath.endsWith("/")) { + appPath = appPath + "/"; + } + + // Try translating the app path into a vold path, but require that it + // belong to the calling package. + String voldPath = maybeTranslatePathForVold(appPath, + userEnv.buildExternalStorageAppDataDirs(callingPkg), + userEnv.buildExternalStorageAppDataDirsForVold(callingPkg)); + if (voldPath != null) { + try { + mConnector.execute("volume", "mkdirs", voldPath); + return 0; + } catch (NativeDaemonConnectorException e) { + return e.getCode(); + } + } + + voldPath = maybeTranslatePathForVold(appPath, + userEnv.buildExternalStorageAppObbDirs(callingPkg), + userEnv.buildExternalStorageAppObbDirsForVold(callingPkg)); + if (voldPath != null) { + try { + mConnector.execute("volume", "mkdirs", voldPath); + return 0; + } catch (NativeDaemonConnectorException e) { + return e.getCode(); + } + } + + throw new SecurityException("Invalid mkdirs path: " + appPath); + } + + /** + * Translate the given path from an app-visible path to a vold-visible path, + * but only if it's under the given whitelisted paths. + * + * @param path a canonicalized app-visible path. + * @param appPaths list of app-visible paths that are allowed. + * @param voldPaths list of vold-visible paths directly corresponding to the + * allowed app-visible paths argument. + * @return a vold-visible path representing the original path, or + * {@code null} if the given path didn't have an app-to-vold + * mapping. + */ + @VisibleForTesting + public static String maybeTranslatePathForVold( + String path, File[] appPaths, File[] voldPaths) { + if (appPaths.length != voldPaths.length) { + throw new IllegalStateException("Paths must be 1:1 mapping"); + } + + for (int i = 0; i < appPaths.length; i++) { + final String appPath = appPaths[i].getAbsolutePath() + "/"; + if (path.startsWith(appPath)) { + path = new File(voldPaths[i], path.substring(appPath.length())) + .getAbsolutePath(); + if (!path.endsWith("/")) { + path = path + "/"; + } + return path; + } + } + return null; + } + + @Override + public StorageVolume[] getVolumeList() { + final int callingUserId = UserHandle.getCallingUserId(); + final boolean accessAll = (mContext.checkPermission( + android.Manifest.permission.ACCESS_ALL_EXTERNAL_STORAGE, + Binder.getCallingPid(), Binder.getCallingUid()) == PERMISSION_GRANTED); + + synchronized (mVolumesLock) { + final ArrayList<StorageVolume> filtered = Lists.newArrayList(); + for (StorageVolume volume : mVolumes) { + final UserHandle owner = volume.getOwner(); + final boolean ownerMatch = owner == null || owner.getIdentifier() == callingUserId; + if (accessAll || ownerMatch) { + filtered.add(volume); + } + } + return filtered.toArray(new StorageVolume[filtered.size()]); + } + } + + private void addObbStateLocked(ObbState obbState) throws RemoteException { + final IBinder binder = obbState.getBinder(); + List<ObbState> obbStates = mObbMounts.get(binder); + + if (obbStates == null) { + obbStates = new ArrayList<ObbState>(); + mObbMounts.put(binder, obbStates); + } else { + for (final ObbState o : obbStates) { + if (o.rawPath.equals(obbState.rawPath)) { + throw new IllegalStateException("Attempt to add ObbState twice. " + + "This indicates an error in the MountService logic."); + } + } + } + + obbStates.add(obbState); + try { + obbState.link(); + } catch (RemoteException e) { + /* + * The binder died before we could link it, so clean up our state + * and return failure. + */ + obbStates.remove(obbState); + if (obbStates.isEmpty()) { + mObbMounts.remove(binder); + } + + // Rethrow the error so mountObb can get it + throw e; + } + + mObbPathToStateMap.put(obbState.rawPath, obbState); + } + + private void removeObbStateLocked(ObbState obbState) { + final IBinder binder = obbState.getBinder(); + final List<ObbState> obbStates = mObbMounts.get(binder); + if (obbStates != null) { + if (obbStates.remove(obbState)) { + obbState.unlink(); + } + if (obbStates.isEmpty()) { + mObbMounts.remove(binder); + } + } + + mObbPathToStateMap.remove(obbState.rawPath); + } + + private class ObbActionHandler extends Handler { + private boolean mBound = false; + private final List<ObbAction> mActions = new LinkedList<ObbAction>(); + + ObbActionHandler(Looper l) { + super(l); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case OBB_RUN_ACTION: { + final ObbAction action = (ObbAction) msg.obj; + + if (DEBUG_OBB) + Slog.i(TAG, "OBB_RUN_ACTION: " + action.toString()); + + // If a bind was already initiated we don't really + // need to do anything. The pending install + // will be processed later on. + if (!mBound) { + // If this is the only one pending we might + // have to bind to the service again. + if (!connectToService()) { + Slog.e(TAG, "Failed to bind to media container service"); + action.handleError(); + return; + } + } + + mActions.add(action); + break; + } + case OBB_MCS_BOUND: { + if (DEBUG_OBB) + Slog.i(TAG, "OBB_MCS_BOUND"); + if (msg.obj != null) { + mContainerService = (IMediaContainerService) msg.obj; + } + if (mContainerService == null) { + // Something seriously wrong. Bail out + Slog.e(TAG, "Cannot bind to media container service"); + for (ObbAction action : mActions) { + // Indicate service bind error + action.handleError(); + } + mActions.clear(); + } else if (mActions.size() > 0) { + final ObbAction action = mActions.get(0); + if (action != null) { + action.execute(this); + } + } else { + // Should never happen ideally. + Slog.w(TAG, "Empty queue"); + } + break; + } + case OBB_MCS_RECONNECT: { + if (DEBUG_OBB) + Slog.i(TAG, "OBB_MCS_RECONNECT"); + if (mActions.size() > 0) { + if (mBound) { + disconnectService(); + } + if (!connectToService()) { + Slog.e(TAG, "Failed to bind to media container service"); + for (ObbAction action : mActions) { + // Indicate service bind error + action.handleError(); + } + mActions.clear(); + } + } + break; + } + case OBB_MCS_UNBIND: { + if (DEBUG_OBB) + Slog.i(TAG, "OBB_MCS_UNBIND"); + + // Delete pending install + if (mActions.size() > 0) { + mActions.remove(0); + } + if (mActions.size() == 0) { + if (mBound) { + disconnectService(); + } + } else { + // There are more pending requests in queue. + // Just post MCS_BOUND message to trigger processing + // of next pending install. + mObbActionHandler.sendEmptyMessage(OBB_MCS_BOUND); + } + break; + } + case OBB_FLUSH_MOUNT_STATE: { + final String path = (String) msg.obj; + + if (DEBUG_OBB) + Slog.i(TAG, "Flushing all OBB state for path " + path); + + synchronized (mObbMounts) { + final List<ObbState> obbStatesToRemove = new LinkedList<ObbState>(); + + final Iterator<ObbState> i = mObbPathToStateMap.values().iterator(); + while (i.hasNext()) { + final ObbState state = i.next(); + + /* + * If this entry's source file is in the volume path + * that got unmounted, remove it because it's no + * longer valid. + */ + if (state.canonicalPath.startsWith(path)) { + obbStatesToRemove.add(state); + } + } + + for (final ObbState obbState : obbStatesToRemove) { + if (DEBUG_OBB) + Slog.i(TAG, "Removing state for " + obbState.rawPath); + + removeObbStateLocked(obbState); + + try { + obbState.token.onObbResult(obbState.rawPath, obbState.nonce, + OnObbStateChangeListener.UNMOUNTED); + } catch (RemoteException e) { + Slog.i(TAG, "Couldn't send unmount notification for OBB: " + + obbState.rawPath); + } + } + } + break; + } + } + } + + private boolean connectToService() { + if (DEBUG_OBB) + Slog.i(TAG, "Trying to bind to DefaultContainerService"); + + Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); + if (mContext.bindService(service, mDefContainerConn, Context.BIND_AUTO_CREATE)) { + mBound = true; + return true; + } + return false; + } + + private void disconnectService() { + mContainerService = null; + mBound = false; + mContext.unbindService(mDefContainerConn); + } + } + + abstract class ObbAction { + private static final int MAX_RETRIES = 3; + private int mRetries; + + ObbState mObbState; + + ObbAction(ObbState obbState) { + mObbState = obbState; + } + + public void execute(ObbActionHandler handler) { + try { + if (DEBUG_OBB) + Slog.i(TAG, "Starting to execute action: " + toString()); + mRetries++; + if (mRetries > MAX_RETRIES) { + Slog.w(TAG, "Failed to invoke remote methods on default container service. Giving up"); + mObbActionHandler.sendEmptyMessage(OBB_MCS_UNBIND); + handleError(); + return; + } else { + handleExecute(); + if (DEBUG_OBB) + Slog.i(TAG, "Posting install MCS_UNBIND"); + mObbActionHandler.sendEmptyMessage(OBB_MCS_UNBIND); + } + } catch (RemoteException e) { + if (DEBUG_OBB) + Slog.i(TAG, "Posting install MCS_RECONNECT"); + mObbActionHandler.sendEmptyMessage(OBB_MCS_RECONNECT); + } catch (Exception e) { + if (DEBUG_OBB) + Slog.d(TAG, "Error handling OBB action", e); + handleError(); + mObbActionHandler.sendEmptyMessage(OBB_MCS_UNBIND); + } + } + + abstract void handleExecute() throws RemoteException, IOException; + abstract void handleError(); + + protected ObbInfo getObbInfo() throws IOException { + ObbInfo obbInfo; + try { + obbInfo = mContainerService.getObbInfo(mObbState.ownerPath); + } catch (RemoteException e) { + Slog.d(TAG, "Couldn't call DefaultContainerService to fetch OBB info for " + + mObbState.ownerPath); + obbInfo = null; + } + if (obbInfo == null) { + throw new IOException("Couldn't read OBB file: " + mObbState.ownerPath); + } + return obbInfo; + } + + protected void sendNewStatusOrIgnore(int status) { + if (mObbState == null || mObbState.token == null) { + return; + } + + try { + mObbState.token.onObbResult(mObbState.rawPath, mObbState.nonce, status); + } catch (RemoteException e) { + Slog.w(TAG, "MountServiceListener went away while calling onObbStateChanged"); + } + } + } + + class MountObbAction extends ObbAction { + private final String mKey; + private final int mCallingUid; + + MountObbAction(ObbState obbState, String key, int callingUid) { + super(obbState); + mKey = key; + mCallingUid = callingUid; + } + + @Override + public void handleExecute() throws IOException, RemoteException { + waitForReady(); + warnOnNotMounted(); + + final ObbInfo obbInfo = getObbInfo(); + + if (!isUidOwnerOfPackageOrSystem(obbInfo.packageName, mCallingUid)) { + Slog.w(TAG, "Denied attempt to mount OBB " + obbInfo.filename + + " which is owned by " + obbInfo.packageName); + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_PERMISSION_DENIED); + return; + } + + final boolean isMounted; + synchronized (mObbMounts) { + isMounted = mObbPathToStateMap.containsKey(mObbState.rawPath); + } + if (isMounted) { + Slog.w(TAG, "Attempt to mount OBB which is already mounted: " + obbInfo.filename); + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_ALREADY_MOUNTED); + return; + } + + final String hashedKey; + if (mKey == null) { + hashedKey = "none"; + } else { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + + KeySpec ks = new PBEKeySpec(mKey.toCharArray(), obbInfo.salt, + PBKDF2_HASH_ROUNDS, CRYPTO_ALGORITHM_KEY_SIZE); + SecretKey key = factory.generateSecret(ks); + BigInteger bi = new BigInteger(key.getEncoded()); + hashedKey = bi.toString(16); + } catch (NoSuchAlgorithmException e) { + Slog.e(TAG, "Could not load PBKDF2 algorithm", e); + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_INTERNAL); + return; + } catch (InvalidKeySpecException e) { + Slog.e(TAG, "Invalid key spec when loading PBKDF2 algorithm", e); + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_INTERNAL); + return; + } + } + + int rc = StorageResultCode.OperationSucceeded; + try { + mConnector.execute("obb", "mount", mObbState.voldPath, new SensitiveArg(hashedKey), + mObbState.ownerGid); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code != VoldResponseCode.OpFailedStorageBusy) { + rc = StorageResultCode.OperationFailedInternalError; + } + } + + if (rc == StorageResultCode.OperationSucceeded) { + if (DEBUG_OBB) + Slog.d(TAG, "Successfully mounted OBB " + mObbState.voldPath); + + synchronized (mObbMounts) { + addObbStateLocked(mObbState); + } + + sendNewStatusOrIgnore(OnObbStateChangeListener.MOUNTED); + } else { + Slog.e(TAG, "Couldn't mount OBB file: " + rc); + + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_COULD_NOT_MOUNT); + } + } + + @Override + public void handleError() { + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_INTERNAL); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MountObbAction{"); + sb.append(mObbState); + sb.append('}'); + return sb.toString(); + } + } + + class UnmountObbAction extends ObbAction { + private final boolean mForceUnmount; + + UnmountObbAction(ObbState obbState, boolean force) { + super(obbState); + mForceUnmount = force; + } + + @Override + public void handleExecute() throws IOException { + waitForReady(); + warnOnNotMounted(); + + final ObbInfo obbInfo = getObbInfo(); + + final ObbState existingState; + synchronized (mObbMounts) { + existingState = mObbPathToStateMap.get(mObbState.rawPath); + } + + if (existingState == null) { + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_NOT_MOUNTED); + return; + } + + if (existingState.ownerGid != mObbState.ownerGid) { + Slog.w(TAG, "Permission denied attempting to unmount OBB " + existingState.rawPath + + " (owned by GID " + existingState.ownerGid + ")"); + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_PERMISSION_DENIED); + return; + } + + int rc = StorageResultCode.OperationSucceeded; + try { + final Command cmd = new Command("obb", "unmount", mObbState.voldPath); + if (mForceUnmount) { + cmd.appendArg("force"); + } + mConnector.execute(cmd); + } catch (NativeDaemonConnectorException e) { + int code = e.getCode(); + if (code == VoldResponseCode.OpFailedStorageBusy) { + rc = StorageResultCode.OperationFailedStorageBusy; + } else if (code == VoldResponseCode.OpFailedStorageNotFound) { + // If it's not mounted then we've already won. + rc = StorageResultCode.OperationSucceeded; + } else { + rc = StorageResultCode.OperationFailedInternalError; + } + } + + if (rc == StorageResultCode.OperationSucceeded) { + synchronized (mObbMounts) { + removeObbStateLocked(existingState); + } + + sendNewStatusOrIgnore(OnObbStateChangeListener.UNMOUNTED); + } else { + Slog.w(TAG, "Could not unmount OBB: " + existingState); + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_COULD_NOT_UNMOUNT); + } + } + + @Override + public void handleError() { + sendNewStatusOrIgnore(OnObbStateChangeListener.ERROR_INTERNAL); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("UnmountObbAction{"); + sb.append(mObbState); + sb.append(",force="); + sb.append(mForceUnmount); + sb.append('}'); + return sb.toString(); + } + } + + @VisibleForTesting + public static String buildObbPath(final String canonicalPath, int userId, boolean forVold) { + // TODO: allow caller to provide Environment for full testing + // TODO: extend to support OBB mounts on secondary external storage + + // Only adjust paths when storage is emulated + if (!Environment.isExternalStorageEmulated()) { + return canonicalPath; + } + + String path = canonicalPath.toString(); + + // First trim off any external storage prefix + final UserEnvironment userEnv = new UserEnvironment(userId); + + // /storage/emulated/0 + final String externalPath = userEnv.getExternalStorageDirectory().getAbsolutePath(); + // /storage/emulated_legacy + final String legacyExternalPath = Environment.getLegacyExternalStorageDirectory() + .getAbsolutePath(); + + if (path.startsWith(externalPath)) { + path = path.substring(externalPath.length() + 1); + } else if (path.startsWith(legacyExternalPath)) { + path = path.substring(legacyExternalPath.length() + 1); + } else { + return canonicalPath; + } + + // Handle special OBB paths on emulated storage + final String obbPath = "Android/obb"; + if (path.startsWith(obbPath)) { + path = path.substring(obbPath.length() + 1); + + if (forVold) { + return new File(Environment.getEmulatedStorageObbSource(), path).getAbsolutePath(); + } else { + final UserEnvironment ownerEnv = new UserEnvironment(UserHandle.USER_OWNER); + return new File(ownerEnv.buildExternalStorageAndroidObbDirs()[0], path) + .getAbsolutePath(); + } + } + + // Handle normal external storage paths + if (forVold) { + return new File(Environment.getEmulatedStorageSource(userId), path).getAbsolutePath(); + } else { + return new File(userEnv.getExternalDirsForApp()[0], path).getAbsolutePath(); + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); + + synchronized (mObbMounts) { + pw.println("mObbMounts:"); + pw.increaseIndent(); + final Iterator<Entry<IBinder, List<ObbState>>> binders = mObbMounts.entrySet() + .iterator(); + while (binders.hasNext()) { + Entry<IBinder, List<ObbState>> e = binders.next(); + pw.println(e.getKey() + ":"); + pw.increaseIndent(); + final List<ObbState> obbStates = e.getValue(); + for (final ObbState obbState : obbStates) { + pw.println(obbState); + } + pw.decreaseIndent(); + } + pw.decreaseIndent(); + + pw.println(); + pw.println("mObbPathToStateMap:"); + pw.increaseIndent(); + final Iterator<Entry<String, ObbState>> maps = mObbPathToStateMap.entrySet().iterator(); + while (maps.hasNext()) { + final Entry<String, ObbState> e = maps.next(); + pw.print(e.getKey()); + pw.print(" -> "); + pw.println(e.getValue()); + } + pw.decreaseIndent(); + } + + synchronized (mVolumesLock) { + pw.println(); + pw.println("mVolumes:"); + pw.increaseIndent(); + for (StorageVolume volume : mVolumes) { + pw.println(volume); + pw.increaseIndent(); + pw.println("Current state: " + mVolumeStates.get(volume.getPath())); + pw.decreaseIndent(); + } + pw.decreaseIndent(); + } + + pw.println(); + pw.println("mConnection:"); + pw.increaseIndent(); + mConnector.dump(fd, pw, args); + pw.decreaseIndent(); + } + + /** {@inheritDoc} */ + public void monitor() { + if (mConnector != null) { + mConnector.monitor(); + } + } +} |