Suchi Amalapurapu 0eec21d97d Add dialog to display storage users when enabling/disabling ums
Some error dialogs and related strings
MountService changes to follow unmount path when enabling ums.

Please note that MountService api setUmsEnabled does not return
error codes for now. This is a known limitation.
2010-02-26 12:48:21 -08:00

1208 lines
46 KiB
Java

/*
* 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 com.android.server.am.ActivityManagerService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.storage.IMountService;
import android.os.storage.IMountServiceListener;
import android.os.storage.StorageResultCode;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.IBinder;
import android.os.Environment;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashSet;
/**
* 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 {
private static final boolean LOCAL_LOGD = false;
private static final String TAG = "MountService";
/*
* 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;
/*
* 600 series - Unsolicited broadcasts.
*/
public static final int VolumeStateChange = 605;
public static final int ShareAvailabilityChange = 620;
public static final int VolumeDiskInserted = 630;
public static final int VolumeDiskRemoved = 631;
public static final int VolumeBadRemoval = 632;
}
private Context mContext;
private NativeDaemonConnector mConnector;
private String mLegacyState = Environment.MEDIA_REMOVED;
private PackageManagerService mPms;
private boolean mUmsEnabling;
// Used as a lock for methods that register/unregister listeners.
final private ArrayList<MountServiceBinderListener> mListeners =
new ArrayList<MountServiceBinderListener>();
private boolean mBooted = false;
private boolean mReady = false;
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>();
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 RETRY_UNMOUNT_DELAY = 30; // in ms
private static final int MAX_UNMOUNT_RETRIES = 4;
private IntentFilter mPmFilter = new IntentFilter(
Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
private BroadcastReceiver mPmReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
mHandler.sendEmptyMessage(H_UNMOUNT_PM_DONE);
}
}
};
class UnmountCallBack {
String path;
int retries;
boolean force;
UnmountCallBack(String path, boolean force) {
retries = 0;
this.path = path;
this.force = force;
}
void handleFinished() {
doUnmountVolume(path, true);
}
}
class UmsEnableCallBack extends UnmountCallBack {
String method;
UmsEnableCallBack(String path, String method, boolean force) {
super(path, force);
this.method = method;
}
@Override
void handleFinished() {
super.handleFinished();
doShareUnshareVolume(path, method, true);
}
}
final private Handler mHandler = new Handler() {
ArrayList<UnmountCallBack> mForceUnmounts = new ArrayList<UnmountCallBack>();
public void handleMessage(Message msg) {
switch (msg.what) {
case H_UNMOUNT_PM_UPDATE: {
UnmountCallBack ucb = (UnmountCallBack) msg.obj;
mForceUnmounts.add(ucb);
mContext.registerReceiver(mPmReceiver, mPmFilter);
boolean hasExtPkgs = mPms.updateExternalMediaStatus(false);
if (!hasExtPkgs) {
// Unregister right away
mHandler.sendEmptyMessage(H_UNMOUNT_PM_DONE);
}
break;
}
case H_UNMOUNT_PM_DONE: {
// Unregister receiver
mContext.unregisterReceiver(mPmReceiver);
UnmountCallBack ucb = mForceUnmounts.get(0);
if (ucb == null || ucb.path == null) {
// Just ignore
return;
}
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 {
// Kill processes holding references first
ActivityManagerService ams = (ActivityManagerService)
ServiceManager.getService("activity");
// Eliminate system process here?
boolean ret = ams.killPidsForMemory(pids);
if (ret) {
// Confirm if file references have been freed.
pids = getStorageUsers(path);
if (pids == null || pids.length == 0) {
done = true;
}
}
}
}
if (done) {
mForceUnmounts.remove(0);
mHandler.sendMessage(mHandler.obtainMessage(H_UNMOUNT_MS,
ucb));
} else {
if (ucb.retries >= MAX_UNMOUNT_RETRIES) {
Log.i(TAG, "Cannot unmount inspite of " +
MAX_UNMOUNT_RETRIES + " to unmount media");
// Send final broadcast indicating failure to unmount.
} else {
mHandler.sendMessageDelayed(
mHandler.obtainMessage(H_UNMOUNT_PM_DONE,
ucb.retries++),
RETRY_UNMOUNT_DELAY);
}
}
break;
}
case H_UNMOUNT_MS : {
UnmountCallBack ucb = (UnmountCallBack) msg.obj;
ucb.handleFinished();
break;
}
}
}
};
private void waitForReady() {
while (mReady == false) {
for (int retries = 5; retries > 0; retries--) {
if (mReady) {
return;
}
SystemClock.sleep(1000);
}
Log.w(TAG, "Waiting too long for mReady!");
}
}
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
mBooted = true;
/*
* In the simulator, we need to broadcast a volume mounted event
* to make the media scanner run.
*/
if ("simulator".equals(SystemProperties.get("ro.product.device"))) {
notifyVolumeStateChange(null, "/sdcard", VolumeState.NoMedia, VolumeState.Mounted);
return;
}
new Thread() {
public void run() {
try {
String path = Environment.getExternalStorageDirectory().getPath();
if (getVolumeState(
Environment.getExternalStorageDirectory().getPath()).equals(
Environment.MEDIA_UNMOUNTED)) {
int rc = doMountVolume(path);
if (rc != StorageResultCode.OperationSucceeded) {
Log.e(TAG, String.format("Boot-time mount failed (%d)", rc));
}
}
/*
* If UMS is connected in boot, send the connected event
* now that we're up.
*/
if (mSendUmsConnectedOnBoot) {
sendUmsIntent(true);
mSendUmsConnectedOnBoot = false;
}
} catch (Exception ex) {
Log.e(TAG, "Boot-time mount exception", ex);
}
}
}.start();
}
}
};
private final class MountServiceBinderListener implements IBinder.DeathRecipient {
final IMountServiceListener mListener;
MountServiceBinderListener(IMountServiceListener listener) {
mListener = listener;
}
public void binderDied() {
if (LOCAL_LOGD) Log.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.doCommand(String.format(
"volume %sshare %s %s", (enable ? "" : "un"), path, method));
} catch (NativeDaemonConnectorException e) {
Log.e(TAG, "Failed to share/unshare", e);
}
}
private void updatePublicVolumeState(String path, String state) {
if (!path.equals(Environment.getExternalStorageDirectory().getPath())) {
Log.w(TAG, "Multiple volumes not currently supported");
return;
}
if (mLegacyState.equals(state)) {
Log.w(TAG, String.format("Duplicate state transition (%s -> %s)", mLegacyState, state));
return;
}
String oldState = mLegacyState;
mLegacyState = state;
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) {
Log.e(TAG, "Listener dead");
mListeners.remove(i);
} catch (Exception ex) {
Log.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() {
public void run() {
/**
* Determine media state and UMS detection status
*/
String path = Environment.getExternalStorageDirectory().getPath();
String state = Environment.MEDIA_REMOVED;
try {
String[] vols = mConnector.doListCommand(
"volume list", VoldResponseCode.VolumeListResult);
for (String volstr : vols) {
String[] tok = volstr.split(" ");
// FMT: <label> <mountpoint> <state>
if (!tok[1].equals(path)) {
Log.w(TAG, String.format(
"Skipping unknown volume '%s'",tok[1]));
continue;
}
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;
Log.i(TAG, "Media already mounted on daemon connection");
} else if (st == VolumeState.Shared) {
state = Environment.MEDIA_SHARED;
Log.i(TAG, "Media shared on daemon connection");
} else {
throw new Exception(String.format("Unexpected state %d", st));
}
}
if (state != null) {
updatePublicVolumeState(path, state);
}
} catch (Exception e) {
Log.e(TAG, "Error processing initial volume state", e);
updatePublicVolumeState(path, Environment.MEDIA_REMOVED);
}
try {
boolean avail = doGetShareMethodAvailable("ums");
notifyShareAvailabilityChange("ums", avail);
} catch (Exception ex) {
Log.w(TAG, "Failed to get share availability");
}
/*
* Now that we've done our initialization, release
* the hounds!
*/
mReady = true;
}
}.start();
}
/**
* Callback from NativeDaemonConnector
*/
public boolean onEvent(int code, String raw, String[] cooked) {
Intent in = null;
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.ShareAvailabilityChange) {
// FMT: NNN Share method <method> now <available|unavailable>
boolean avail = false;
if (cooked[5].equals("available")) {
avail = true;
}
notifyShareAvailabilityChange(cooked[3], avail);
} 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>)
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) {
Log.e(TAG, "Failed to parse major/minor", ex);
}
if (code == VoldResponseCode.VolumeDiskInserted) {
new Thread() {
public void run() {
try {
int rc;
if ((rc = doMountVolume(path)) != StorageResultCode.OperationSucceeded) {
Log.w(TAG, String.format("Insertion mount failed (%d)", rc));
}
} catch (Exception ex) {
Log.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 */
updatePublicVolumeState(path, Environment.MEDIA_UNMOUNTED);
in = new Intent(Intent.ACTION_MEDIA_UNMOUNTED, Uri.parse("file://" + path));
mContext.sendBroadcast(in);
updatePublicVolumeState(path, Environment.MEDIA_REMOVED);
in = new Intent(Intent.ACTION_MEDIA_REMOVED, Uri.parse("file://" + path));
} else if (code == VoldResponseCode.VolumeBadRemoval) {
/* Send the media unmounted event first */
updatePublicVolumeState(path, Environment.MEDIA_UNMOUNTED);
in = new Intent(Intent.ACTION_MEDIA_UNMOUNTED, Uri.parse("file://" + path));
mContext.sendBroadcast(in);
updatePublicVolumeState(path, Environment.MEDIA_BAD_REMOVAL);
in = new Intent(Intent.ACTION_MEDIA_BAD_REMOVAL, Uri.parse("file://" + path));
} else {
Log.e(TAG, String.format("Unknown code {%d}", code));
}
} else {
return false;
}
if (in != null) {
mContext.sendBroadcast(in);
}
return true;
}
private void notifyVolumeStateChange(String label, String path, int oldState, int newState) {
String vs = getVolumeState(path);
Intent in = null;
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 (!vs.equals(
Environment.MEDIA_BAD_REMOVAL) && !vs.equals(
Environment.MEDIA_NOFS) && !vs.equals(
Environment.MEDIA_UNMOUNTABLE) && !getUmsEnabling()) {
updatePublicVolumeState(path, Environment.MEDIA_UNMOUNTED);
in = new Intent(Intent.ACTION_MEDIA_UNMOUNTED, Uri.parse("file://" + path));
}
} else if (newState == VolumeState.Pending) {
} else if (newState == VolumeState.Checking) {
updatePublicVolumeState(path, Environment.MEDIA_CHECKING);
in = new Intent(Intent.ACTION_MEDIA_CHECKING, Uri.parse("file://" + path));
} else if (newState == VolumeState.Mounted) {
updatePublicVolumeState(path, Environment.MEDIA_MOUNTED);
// Update media status on PackageManagerService to mount packages on sdcard
mPms.updateExternalMediaStatus(true);
in = new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://" + path));
in.putExtra("read-only", false);
} else if (newState == VolumeState.Unmounting) {
mPms.updateExternalMediaStatus(false);
in = new Intent(Intent.ACTION_MEDIA_EJECT, Uri.parse("file://" + path));
} else if (newState == VolumeState.Formatting) {
} else if (newState == VolumeState.Shared) {
/* Send the media unmounted event first */
updatePublicVolumeState(path, Environment.MEDIA_UNMOUNTED);
in = new Intent(Intent.ACTION_MEDIA_UNMOUNTED, Uri.parse("file://" + path));
mContext.sendBroadcast(in);
updatePublicVolumeState(path, Environment.MEDIA_SHARED);
in = new Intent(Intent.ACTION_MEDIA_SHARED, Uri.parse("file://" + path));
} else if (newState == VolumeState.SharedMnt) {
Log.e(TAG, "Live shared mounts not supported yet!");
return;
} else {
Log.e(TAG, "Unhandled VolumeState {" + newState + "}");
}
if (in != null) {
mContext.sendBroadcast(in);
}
}
private boolean doGetShareMethodAvailable(String method) {
ArrayList<String> rsp = mConnector.doCommand("share status " + method);
for (String line : rsp) {
String []tok = line.split(" ");
int code;
try {
code = Integer.parseInt(tok[0]);
} catch (NumberFormatException nfe) {
Log.e(TAG, String.format("Error parsing code %s", tok[0]));
return false;
}
if (code == VoldResponseCode.ShareStatusResult) {
if (tok[2].equals("available"))
return true;
return false;
} else {
Log.e(TAG, String.format("Unexpected response code %d", code));
return false;
}
}
Log.e(TAG, "Got an empty response");
return false;
}
private int doMountVolume(String path) {
int rc = StorageResultCode.OperationSucceeded;
try {
mConnector.doCommand(String.format("volume mount %s", path));
} catch (NativeDaemonConnectorException e) {
/*
* Mount failed for some reason
*/
Intent in = null;
int code = e.getCode();
if (code == VoldResponseCode.OpFailedNoMedia) {
/*
* Attempt to mount but no media inserted
*/
rc = StorageResultCode.OperationFailedNoMedia;
} else if (code == VoldResponseCode.OpFailedMediaBlank) {
/*
* Media is blank or does not contain a supported filesystem
*/
updatePublicVolumeState(path, Environment.MEDIA_NOFS);
in = new Intent(Intent.ACTION_MEDIA_NOFS, Uri.parse("file://" + path));
rc = StorageResultCode.OperationFailedMediaBlank;
} else if (code == VoldResponseCode.OpFailedMediaCorrupt) {
/*
* Volume consistency check failed
*/
updatePublicVolumeState(path, Environment.MEDIA_UNMOUNTABLE);
in = new Intent(Intent.ACTION_MEDIA_UNMOUNTABLE, Uri.parse("file://" + path));
rc = StorageResultCode.OperationFailedMediaCorrupt;
} else {
rc = StorageResultCode.OperationFailedInternalError;
}
/*
* Send broadcast intent (if required for the failure)
*/
if (in != null) {
mContext.sendBroadcast(in);
}
}
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.
*/
private int doUnmountVolume(String path, boolean force) {
if (!getVolumeState(path).equals(Environment.MEDIA_MOUNTED)) {
return VoldResponseCode.OpFailedVolNotMounted;
}
// We unmounted the volume. No of the asec containers are available now.
synchronized (mAsecMountSet) {
mAsecMountSet.clear();
}
// Notify PackageManager of potential media removal and deal with
// return code later on. The caller of this api should be aware or have been
// notified that the applications installed on the media will be killed.
// Redundant probably. But no harm in updating state again.
mPms.updateExternalMediaStatus(false);
try {
mConnector.doCommand(String.format(
"volume unmount %s%s", path, (force ? " force" : "")));
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 {
String cmd = String.format("volume format %s", path);
mConnector.doCommand(cmd);
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) {
String cmd = String.format("volume shared %s %s", path, method);
ArrayList<String> rsp = mConnector.doCommand(cmd);
for (String line : rsp) {
String []tok = line.split(" ");
int code;
try {
code = Integer.parseInt(tok[0]);
} catch (NumberFormatException nfe) {
Log.e(TAG, String.format("Error parsing code %s", tok[0]));
return false;
}
if (code == VoldResponseCode.ShareEnabledResult) {
if (tok[2].equals("enabled"))
return true;
return false;
} else {
Log.e(TAG, String.format("Unexpected response code %d", code));
return false;
}
}
Log.e(TAG, "Got an empty response");
return false;
}
private void notifyShareAvailabilityChange(String method, final boolean avail) {
if (!method.equals("ums")) {
Log.w(TAG, "Ignoring unsupported share method {" + method + "}");
return;
}
synchronized (mListeners) {
for (int i = mListeners.size() -1; i >= 0; i--) {
MountServiceBinderListener bl = mListeners.get(i);
try {
bl.mListener.onUsbMassStorageConnectionChanged(avail);
} catch (RemoteException rex) {
Log.e(TAG, "Listener dead");
mListeners.remove(i);
} catch (Exception ex) {
Log.e(TAG, "Listener failed", ex);
}
}
}
if (mBooted == true) {
sendUmsIntent(avail);
} else {
mSendUmsConnectedOnBoot = avail;
}
}
private void sendUmsIntent(boolean c) {
mContext.sendBroadcast(
new Intent((c ? Intent.ACTION_UMS_CONNECTED : Intent.ACTION_UMS_DISCONNECTED)));
}
private void validatePermission(String perm) {
if (mContext.checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(String.format("Requires %s permission", perm));
}
}
/**
* Constructs a new MountService instance
*
* @param context Binder context for this service
*/
public MountService(Context context) {
mContext = context;
// XXX: This will go away soon in favor of IMountServiceObserver
mPms = (PackageManagerService) ServiceManager.getService("package");
mContext.registerReceiver(mBroadcastReceiver,
new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null);
/*
* Vold does not run in the simulator, so pretend the connector thread
* ran and did its thing.
*/
if ("simulator".equals(SystemProperties.get("ro.product.device"))) {
mReady = true;
mUmsEnabling = true;
return;
}
mConnector = new NativeDaemonConnector(this, "vold", 10, "VoldConnector");
mReady = false;
Thread thread = new Thread(mConnector, NativeDaemonConnector.class.getName());
thread.start();
}
/**
* 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) {
Log.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));
return;
}
}
}
}
public void shutdown() {
validatePermission(android.Manifest.permission.SHUTDOWN);
Log.i(TAG, "Shutting down");
String path = Environment.getExternalStorageDirectory().getPath();
String state = getVolumeState(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) {
Log.e(TAG, "Interrupted while waiting for media", iex);
break;
}
state = Environment.getExternalStorageState();
}
if (retries == 0) {
Log.e(TAG, "Timed out waiting for media to check");
}
}
if (state.equals(Environment.MEDIA_MOUNTED)) {
/*
* If the media is mounted, then gracefully unmount it.
*/
if (doUnmountVolume(path, true) != StorageResultCode.OperationSucceeded) {
Log.e(TAG, "Failed to unmount media for shutdown");
}
}
}
private boolean getUmsEnabling() {
synchronized (mListeners) {
return mUmsEnabling;
}
}
private void setUmsEnabling(boolean enable) {
synchronized (mListeners) {
mUmsEnabling = true;
}
}
public boolean isUsbMassStorageConnected() {
waitForReady();
if (getUmsEnabling()) {
return true;
}
return doGetShareMethodAvailable("ums");
}
public void setUsbMassStorageEnabled(boolean enable) {
waitForReady();
validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
// TODO: Add support for multiple share methods
/*
* If the volume is mounted and we're enabling then unmount it
*/
String path = Environment.getExternalStorageDirectory().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) {
Log.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();
return doGetVolumeShared(Environment.getExternalStorageDirectory().getPath(), "ums");
}
/**
* @return state of the volume at the specified mount point
*/
public String getVolumeState(String mountPoint) {
/*
* XXX: Until we have multiple volume discovery, just hardwire
* this to /sdcard
*/
if (!mountPoint.equals(Environment.getExternalStorageDirectory().getPath())) {
Log.w(TAG, "getVolumeState(" + mountPoint + "): Unknown volume");
throw new IllegalArgumentException();
}
return mLegacyState;
}
public int mountVolume(String path) {
validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
waitForReady();
return doMountVolume(path);
}
public void unmountVolume(String path, boolean force) {
validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
waitForReady();
UnmountCallBack ucb = new UnmountCallBack(path, force);
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 {
String[] r = mConnector.doListCommand(
String.format("storage users %s", 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) {
Log.e(TAG, String.format("Error parsing pid %s", tok[0]));
return new int[0];
}
}
return data;
} catch (NativeDaemonConnectorException e) {
Log.e(TAG, "Failed to retrieve storage users list", e);
return new int[0];
}
}
private void warnOnNotMounted() {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.w(TAG, "getSecureContainerList() called when storage not mounted");
}
}
public String[] getSecureContainerList() {
validatePermission(android.Manifest.permission.ASEC_ACCESS);
waitForReady();
warnOnNotMounted();
try {
return mConnector.doListCommand("asec list", VoldResponseCode.AsecListResult);
} catch (NativeDaemonConnectorException e) {
return new String[0];
}
}
public int createSecureContainer(String id, int sizeMb, String fstype,
String key, int ownerUid) {
validatePermission(android.Manifest.permission.ASEC_CREATE);
waitForReady();
warnOnNotMounted();
int rc = StorageResultCode.OperationSucceeded;
String cmd = String.format("asec create %s %d %s %s %d", id, sizeMb, fstype, key, ownerUid);
try {
mConnector.doCommand(cmd);
} 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.doCommand(String.format("asec finalize %s", id));
/*
* Finalization 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();
int rc = StorageResultCode.OperationSucceeded;
try {
mConnector.doCommand(String.format("asec destroy %s%s", id, (force ? " force" : "")));
} 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;
String cmd = String.format("asec mount %s %s %d", id, key, ownerUid);
try {
mConnector.doCommand(cmd);
} catch (NativeDaemonConnectorException e) {
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;
}
}
int rc = StorageResultCode.OperationSucceeded;
String cmd = String.format("asec unmount %s%s", id, (force ? " force" : ""));
try {
mConnector.doCommand(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;
String cmd = String.format("asec rename %s %s", oldId, newId);
try {
mConnector.doCommand(cmd);
} catch (NativeDaemonConnectorException e) {
rc = StorageResultCode.OperationFailedInternalError;
}
if (rc == StorageResultCode.OperationSucceeded) {
synchronized (mAsecMountSet) {
if (!mAsecMountSet.contains(newId)) {
mAsecMountSet.add(newId);
}
}
}
return rc;
}
public String getSecureContainerPath(String id) {
validatePermission(android.Manifest.permission.ASEC_ACCESS);
waitForReady();
warnOnNotMounted();
ArrayList<String> rsp = mConnector.doCommand("asec path " + id);
for (String line : rsp) {
String []tok = line.split(" ");
int code = Integer.parseInt(tok[0]);
if (code == VoldResponseCode.AsecPathResult) {
return tok[1];
} else {
Log.e(TAG, String.format("Unexpected response code %d", code));
return "";
}
}
Log.e(TAG, "Got an empty response");
return "";
}
}