/* * 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 android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.Uri; import android.os.IMountService; import android.os.Environment; import android.os.ServiceManager; import android.os.SystemProperties; import android.os.UEventObserver; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; import android.provider.Settings; import android.content.ContentResolver; import android.database.ContentObserver; import java.io.File; import java.io.FileReader; import java.lang.IllegalStateException; /** * MountService implements an to the mount service daemon * @hide */ class MountService extends IMountService.Stub implements INativeDaemonConnectorCallbacks { private static final String TAG = "MountService"; 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; } class VoldResponseCode { public static final int VolumeListResult = 110; public static final int AsecListResult = 111; public static final int ShareAvailabilityResult = 210; public static final int AsecPathResult = 211; public static final int VolumeStateChange = 605; public static final int VolumeMountFailedBlank = 610; public static final int VolumeMountFailedDamaged = 611; public static final int VolumeMountFailedNoMedia = 612; public static final int ShareAvailabilityChange = 620; public static final int VolumeDiskInserted = 630; public static final int VolumeDiskRemoved = 631; public static final int VolumeBadRemoval = 632; } /** * Binder context for this service */ private Context mContext; /** * connectorr object for communicating with vold */ private NativeDaemonConnector mConnector; /** * The notification that is shown when a USB mass storage host * is connected. *

* This is lazily created, so use {@link #setUsbStorageNotification()}. */ private Notification mUsbStorageNotification; /** * The notification that is shown when the following media events occur: * - Media is being checked * - Media is blank (or unknown filesystem) * - Media is corrupt * - Media is safe to unmount * - Media is missing *

* This is lazily created, so use {@link #setMediaStorageNotification()}. */ private Notification mMediaStorageNotification; private boolean mShowSafeUnmountNotificationWhenUnmounted; private boolean mPlaySounds; private boolean mMounted; private SettingsWatcher mSettingsWatcher; private boolean mAutoStartUms; private boolean mPromptUms; private boolean mUmsActiveNotify; private boolean mUmsConnected = false; private boolean mUmsEnabled = false; private boolean mUmsEnabling = false; private String mLegacyState = Environment.MEDIA_REMOVED; private PackageManagerService mPms; /** * Constructs a new MountService instance * * @param context Binder context for this service */ public MountService(Context context) { mContext = context; mPms = (PackageManagerService) ServiceManager.getService("package"); // Register a BOOT_COMPLETED handler so that we can start // our NativeDaemonConnector. We defer the startup so that we don't // start processing events before we ought-to mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null); mConnector = new NativeDaemonConnector(this, "vold", 10, "VoldConnector"); mShowSafeUnmountNotificationWhenUnmounted = false; mPlaySounds = SystemProperties.get("persist.service.mount.playsnd", "1").equals("1"); ContentResolver cr = mContext.getContentResolver(); mAutoStartUms = (Settings.Secure.getInt( cr, Settings.Secure.MOUNT_UMS_AUTOSTART, 0) == 1); mPromptUms = (Settings.Secure.getInt( cr, Settings.Secure.MOUNT_UMS_PROMPT, 1) == 1); mUmsActiveNotify = (Settings.Secure.getInt( cr, Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED, 1) == 1); mSettingsWatcher = new SettingsWatcher(new Handler()); } private class SettingsWatcher extends ContentObserver { public SettingsWatcher(Handler handler) { super(handler); ContentResolver cr = mContext.getContentResolver(); cr.registerContentObserver(Settings.System.getUriFor( Settings.Secure.MOUNT_PLAY_NOTIFICATION_SND), false, this); cr.registerContentObserver(Settings.Secure.getUriFor( Settings.Secure.MOUNT_UMS_AUTOSTART), false, this); cr.registerContentObserver(Settings.Secure.getUriFor( Settings.Secure.MOUNT_UMS_PROMPT), false, this); cr.registerContentObserver(Settings.Secure.getUriFor( Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED), false, this); } public void onChange(boolean selfChange) { super.onChange(selfChange); ContentResolver cr = mContext.getContentResolver(); boolean newPlayNotificationSounds = (Settings.Secure.getInt( cr, Settings.Secure.MOUNT_PLAY_NOTIFICATION_SND, 1) == 1); boolean newUmsAutostart = (Settings.Secure.getInt( cr, Settings.Secure.MOUNT_UMS_AUTOSTART, 0) == 1); if (newUmsAutostart != mAutoStartUms) { mAutoStartUms = newUmsAutostart; } boolean newUmsPrompt = (Settings.Secure.getInt( cr, Settings.Secure.MOUNT_UMS_PROMPT, 1) == 1); if (newUmsPrompt != mPromptUms) { mPromptUms = newUmsAutostart; } boolean newUmsNotifyEnabled = (Settings.Secure.getInt( cr, Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED, 1) == 1); if (mUmsEnabled) { if (newUmsNotifyEnabled) { Intent intent = new Intent(); intent.setClass(mContext, com.android.internal.app.UsbStorageStopActivity.class); PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); setUsbStorageNotification(com.android.internal.R.string.usb_storage_stop_notification_title, com.android.internal.R.string.usb_storage_stop_notification_message, com.android.internal.R.drawable.stat_sys_warning, false, true, pi); } else { setUsbStorageNotification(0, 0, 0, false, false, null); } } if (newUmsNotifyEnabled != mUmsActiveNotify) { mUmsActiveNotify = newUmsNotifyEnabled; } } } BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { /* * Vold does not run in the simulator, so fake out a mounted * event to trigger MediaScanner */ if ("simulator".equals(SystemProperties.get("ro.product.device"))) { notifyMediaMounted( Environment.getExternalStorageDirectory().getPath(), false); return; } Thread thread = new Thread( mConnector, NativeDaemonConnector.class.getName()); thread.start(); } } }; public void shutdown() { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.SHUTDOWN) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires SHUTDOWN permission"); } Log.d(TAG, "Shutting down"); String state = Environment.getExternalStorageState(); 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. */ try { setMassStorageEnabled(false); } catch (Exception e) { Log.e(TAG, "ums disable failed", e); } } 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. */ try { String m = Environment.getExternalStorageDirectory().toString(); unmountVolume(m); int retries = 12; while (!state.equals(Environment.MEDIA_UNMOUNTED) && (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 unmount"); } } catch (Exception e) { Log.e(TAG, "external storage unmount failed", e); } } } /** * @return true if USB mass storage support is enabled. */ public boolean getMassStorageEnabled() { return mUmsEnabled; } /** * Enables or disables USB mass storage support. * * @param enable true to enable USB mass storage support */ public void setMassStorageEnabled(boolean enable) throws IllegalStateException { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MOUNT_UNMOUNT_FILESYSTEMS permission"); } try { String vp = Environment.getExternalStorageDirectory().getPath(); String vs = getVolumeState(vp); mUmsEnabling = enable; if (enable && vs.equals(Environment.MEDIA_MOUNTED)) { unmountVolume(vp); mUmsEnabling = false; updateUsbMassStorageNotification(true, false); } setShareMethodEnabled(vp, "ums", enable); mUmsEnabled = enable; mUmsEnabling = false; if (!enable) { mountVolume(vp); if (mPromptUms) { updateUsbMassStorageNotification(false, false); } else { updateUsbMassStorageNotification(true, false); } } } catch (IllegalStateException rex) { Log.e(TAG, "Failed to set ums enable {" + enable + "}"); return; } } /** * @return true if USB mass storage is connected. */ public boolean getMassStorageConnected() { return mUmsConnected; } /** * @return state of the volume at the specified mount point */ public String getVolumeState(String mountPoint) throws IllegalStateException { /* * 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; } /** * Attempt to mount external media */ public void mountVolume(String mountPath) throws IllegalStateException { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MOUNT_UNMOUNT_FILESYSTEMS permission"); } mConnector.doCommand(String.format("mount %s", mountPath)); } /** * Attempt to unmount external media to prepare for eject */ public void unmountVolume(String mountPath) throws IllegalStateException { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MOUNT_UNMOUNT_FILESYSTEMS permission"); } // Set a flag so that when we get the unmounted event, we know // to display the notification mShowSafeUnmountNotificationWhenUnmounted = true; mConnector.doCommand(String.format("unmount %s", mountPath)); } /** * Attempt to format external media */ public void formatVolume(String formatPath) throws IllegalStateException { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MOUNT_FORMAT_FILESYSTEMS permission"); } mConnector.doCommand(String.format("format %s", formatPath)); } boolean getShareAvailable(String method) throws IllegalStateException { ArrayList rsp = mConnector.doCommand("share_available " + method); for (String line : rsp) { String []tok = line.split(" "); int code = Integer.parseInt(tok[0]); if (code == VoldResponseCode.ShareAvailabilityResult) { if (tok[2].equals("available")) return true; return false; } else { throw new IllegalStateException(String.format("Unexpected response code %d", code)); } } throw new IllegalStateException("Got an empty response"); } /** * Enables or disables USB mass storage support. * * @param enable true to enable USB mass storage support */ void setShareMethodEnabled(String mountPoint, String method, boolean enable) throws IllegalStateException { mConnector.doCommand(String.format( "%sshare %s %s", (enable ? "" : "un"), mountPoint, method)); } /** * Returns true if we're playing media notification sounds. */ public boolean getPlayNotificationSounds() { return mPlaySounds; } /** * Set whether or not we're playing media notification sounds. */ public void setPlayNotificationSounds(boolean enabled) { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.WRITE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires WRITE_SETTINGS permission"); } mPlaySounds = enabled; SystemProperties.set("persist.service.mount.playsnd", (enabled ? "1" : "0")); } void updatePublicVolumeState(String mountPoint, String state) { if (!mountPoint.equals(Environment.getExternalStorageDirectory().getPath())) { Log.w(TAG, "Multiple volumes not currently supported"); return; } Log.i(TAG, "State for {" + mountPoint + "} = {" + state + "}"); mLegacyState = state; } /** * Update the state of the USB mass storage notification */ void updateUsbMassStorageNotification(boolean suppressIfConnected, boolean sound) { try { if (getMassStorageConnected() && !suppressIfConnected) { Intent intent = new Intent(); intent.setClass(mContext, com.android.internal.app.UsbStorageActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); setUsbStorageNotification( com.android.internal.R.string.usb_storage_notification_title, com.android.internal.R.string.usb_storage_notification_message, com.android.internal.R.drawable.stat_sys_data_usb, sound, true, pi); } else { setUsbStorageNotification(0, 0, 0, false, false, null); } } catch (IllegalStateException e) { // Nothing to do } } void handlePossibleExplicitUnmountBroadcast(String path) { if (mMounted) { mMounted = false; // Update media status on PackageManagerService to unmount packages on sdcard mPms.updateExternalMediaStatus(false); Intent intent = new Intent(Intent.ACTION_MEDIA_UNMOUNTED, Uri.parse("file://" + path)); mContext.sendBroadcast(intent); } } /** * * Callback from NativeDaemonConnector */ public void onDaemonConnected() { new Thread() { public void run() { try { if (!getVolumeState(Environment.getExternalStorageDirectory().getPath()) .equals(Environment.MEDIA_MOUNTED)) { try { mountVolume(Environment.getExternalStorageDirectory().getPath()); } catch (Exception ex) { Log.w(TAG, "Connection-mount failed"); } } else { Log.d(TAG, "Skipping connection-mount; already mounted"); } } catch (IllegalStateException rex) { Log.e(TAG, "Exception while handling connection mount ", rex); } try { boolean avail = getShareAvailable("ums"); notifyShareAvailabilityChange("ums", avail); } catch (Exception ex) { Log.w(TAG, "Failed to get share availability"); } } }.start(); } /** * * Callback from NativeDaemonConnector */ public boolean onEvent(int code, String raw, String[] cooked) { // Log.d(TAG, "event {" + raw + "}"); if (code == VoldResponseCode.VolumeStateChange) { // FMT: NNN Volume