/* * Copyright (C) 2011 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 an * limitations under the License. */ package com.android.server.usb; import android.app.PendingIntent; import android.app.Notification; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbManager; import android.os.FileUtils; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.os.SystemProperties; import android.os.UEventObserver; import android.provider.Settings; import android.util.Slog; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; /** * LegacyUsbDeviceManager manages USB state in devices with legacy USB stacks. */ public class LegacyUsbDeviceManager extends UsbDeviceManager { private static final String TAG = LegacyUsbDeviceManager.class.getSimpleName(); private static final boolean DEBUG = false; private static final String USB_CONNECTED_MATCH = "DEVPATH=/devices/virtual/switch/usb_connected"; private static final String USB_CONFIGURATION_MATCH = "DEVPATH=/devices/virtual/switch/usb_configuration"; private static final String USB_LEGACY_MATCH = "DEVPATH=/devices/virtual/switch/usb_mass_storage"; private static final String USB_CONNECTED_PATH = "/sys/class/switch/usb_connected/state"; private static final String USB_CONFIGURATION_PATH = "/sys/class/switch/usb_configuration/state"; private static final String USB_LEGACY_PATH = "/sys/class/switch/usb_mass_storage/state"; private static final String FUNCTIONS_PATH = "/sys/devices/virtual/usb_composite/"; private static final String MASS_STORAGE_FILE_PATH = Resources.getSystem().getString(com.android.internal.R.string.config_legacyUmsLunFile); private static final int MSG_UPDATE_STATE = 0; private static final int MSG_ENABLE_ADB = 1; private static final int MSG_SET_CURRENT_FUNCTION = 2; private static final int MSG_SYSTEM_READY = 3; private static final int MSG_BOOT_COMPLETED = 4; private boolean mConnected = false; private boolean mConfigured = false; // Delay for debouncing USB disconnects. // We often get rapid connect/disconnect events when enabling USB functions, // which need debouncing. private static final int UPDATE_DELAY = 1000; private LegacyUsbHandler mHandler; private boolean mBootCompleted; private final Context mContext; private final ContentResolver mContentResolver; private final UsbSettingsManager mSettingsManager; private NotificationManager mNotificationManager; private final boolean mHasUsbAccessory; private boolean mUseUsbNotification; private boolean mAdbEnabled; private boolean mLegacy = false; private class AdbSettingsObserver extends ContentObserver { public AdbSettingsObserver() { super(null); } @Override public void onChange(boolean selfChange) { boolean enable = (Settings.Secure.getInt(mContentResolver, Settings.Secure.ADB_ENABLED, 0) > 0); mHandler.sendMessage(MSG_ENABLE_ADB, enable); } } /* * Listens for uevent messages from the kernel to monitor the USB state */ private final UEventObserver mUEventObserver = new UEventObserver() { @Override public void onUEvent(UEventObserver.UEvent event) { if (DEBUG) Slog.v(TAG, "USB UEVENT: " + event.toString()); String name = event.get("SWITCH_NAME"); String state = event.get("SWITCH_STATE"); if (name != null && state != null) { if (mLegacy) { if ("usb_mass_storage".equals(name)) { mConnected = "online".equals(state); mConfigured = "online".equals(state); } } else { if ("usb_connected".equals(name)) mConnected = "1".equals(state); else if ("usb_configuration".equals(name)) mConfigured = "1".equals(state); } if (!mConnected && !mConfigured) mHandler.updateState("DISCONNECTED"); else if(mConnected && !mConfigured) mHandler.updateState("CONNECTED"); else if(mConnected && mConfigured) mHandler.updateState("CONFIGURED"); else mHandler.updateState("UNKNOWN"); } } }; public LegacyUsbDeviceManager(Context context, UsbSettingsManager settingsManager) { super(); mContext = context; mContentResolver = context.getContentResolver(); mSettingsManager = settingsManager; PackageManager pm = mContext.getPackageManager(); mHasUsbAccessory = pm.hasSystemFeature(PackageManager.FEATURE_USB_ACCESSORY); // create a thread for our Handler HandlerThread thread = new HandlerThread("LegacyUsbDeviceManager", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mHandler = new LegacyUsbHandler(thread.getLooper()); } @Override public void systemReady() { if (DEBUG) Slog.d(TAG, "systemReady"); mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); // We do not show the USB notification if the primary volume supports mass storage. // The legacy mass storage UI will be used instead. boolean massStorageSupported = false; StorageManager storageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); StorageVolume[] volumes = storageManager.getVolumeList(); if (volumes.length > 0) { massStorageSupported = volumes[0].allowMassStorage(); } mUseUsbNotification = !massStorageSupported; // make sure the ADB_ENABLED setting value matches the current state Settings.Secure.putInt(mContentResolver, Settings.Secure.ADB_ENABLED, mAdbEnabled ? 1 : 0); if (DEBUG) Slog.d(TAG, "mAdbEnable="+mAdbEnabled); mHandler.sendEmptyMessage(MSG_SYSTEM_READY); } private static String addFunction(String functions, String function) { if (!containsFunction(functions, function)) { if (functions.length() > 0) { functions += ","; } functions += function; } return functions; } private static String removeFunction(String functions, String function) { String[] split = functions.split(","); for (int i = 0; i < split.length; i++) { if (function.equals(split[i])) { split[i] = null; } } StringBuilder builder = new StringBuilder(); for (int i = 0; i < split.length; i++) { String s = split[i]; if (s != null) { if (builder.length() > 0) { builder.append(","); } builder.append(s); } } return builder.toString(); } private static boolean containsFunction(String functions, String function) { int index = functions.indexOf(function); if (index < 0) return false; if (index > 0 && functions.charAt(index - 1) != ',') return false; int charAfter = index + function.length(); if (charAfter < functions.length() && functions.charAt(charAfter) != ',') return false; return true; } private final class LegacyUsbHandler extends Handler { // current USB state private boolean mConnected = false; private boolean mConfigured = false; private String mCurrentFunctions; private String mDefaultFunctions; private UsbAccessory mCurrentAccessory; private int mUsbNotificationId; private boolean mAdbNotificationShown; final BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { if (DEBUG) Slog.d(TAG, "boot completed"); mHandler.sendEmptyMessage(MSG_BOOT_COMPLETED); } }; public LegacyUsbHandler(Looper looper) { super(looper); char[] buffer = new char[1024]; try { // persist.sys.usb.config should never be unset. But if it is, set it to "adb" // so we have a chance of debugging what happened. mDefaultFunctions = SystemProperties.get("persist.sys.usb.config", "adb"); // sanity check the sys.usb.config system property // this may be necessary if we crashed while switching USB configurations String config = SystemProperties.get("sys.usb.config", "none"); if (!config.equals(mDefaultFunctions)) { Slog.w(TAG, "resetting config to persistent property: " + mDefaultFunctions); SystemProperties.set("sys.usb.config", mDefaultFunctions); } // Read initial USB state (device mode) try { FileReader file = new FileReader(USB_CONNECTED_PATH); int len = file.read(buffer, 0, 1024); file.close(); mConnected = "1".equals((new String(buffer, 0, len)).trim()); file = new FileReader(USB_CONFIGURATION_PATH); len = file.read(buffer, 0, 1024); file.close(); mConfigured = "1".equals((new String(buffer, 0, len)).trim()); } catch (FileNotFoundException e) { Slog.i(TAG, "This kernel does not have USB configuration switch support"); Slog.i(TAG, "Trying legacy USB configuration switch support"); try { FileReader file = new FileReader(USB_LEGACY_PATH); int len = file.read(buffer, 0, 1024); file.close(); mConnected = "online".equals((new String(buffer, 0, len)).trim()); mLegacy = true; mConfigured = false; } catch (FileNotFoundException f) { Slog.i(TAG, "This kernel does not have legacy USB configuration switch support"); } catch (Exception f) { Slog.e(TAG, "" , f); } } mCurrentFunctions = mDefaultFunctions; if (!mConnected && !mConfigured) updateState("DISCONNECTED"); else if(mConnected && !mConfigured) updateState("CONNECTED"); else if(mConnected && mConfigured) updateState("CONFIGURED"); else updateState("UNKNOWN"); mAdbEnabled = containsFunction(mCurrentFunctions, UsbManager.USB_FUNCTION_ADB); // Upgrade step for previous versions that used persist.service.adb.enable String value = SystemProperties.get("persist.service.adb.enable", "0"); if (value.length() > 0) { char enable = value.charAt(0); if (enable == '1') { setAdbEnabled(true); } else if (enable == '0') { setAdbEnabled(false); } } // register observer to listen for settings changes mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ADB_ENABLED), false, new AdbSettingsObserver()); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ADB_NOTIFY), false, new ContentObserver(null) { public void onChange(boolean selfChange) { updateAdbNotification(); } }); // Watch for USB configuration changes if (mLegacy) { mUEventObserver.startObserving(USB_LEGACY_MATCH); } else { mUEventObserver.startObserving(USB_CONNECTED_MATCH); mUEventObserver.startObserving(USB_CONFIGURATION_MATCH); } mContext.registerReceiver(mBootCompletedReceiver, new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); if(DEBUG) Slog.d(TAG, "Initialised USB event listeners"); } catch (Exception e) { Slog.e(TAG, "Error initializing listener", e); } } public boolean isConnected() { return mConnected; } public boolean isConfigured() { return mConfigured; } public void sendMessage(int what, boolean arg) { removeMessages(what); Message m = Message.obtain(this, what); m.arg1 = (arg ? 1 : 0); sendMessage(m); } public void sendMessage(int what, Object arg) { removeMessages(what); Message m = Message.obtain(this, what); m.obj = arg; sendMessage(m); } public void sendMessage(int what, Object arg0, boolean arg1) { removeMessages(what); Message m = Message.obtain(this, what); m.obj = arg0; m.arg1 = (arg1 ? 1 : 0); sendMessage(m); } public void updateState(String state) { int connected, configured; if ("DISCONNECTED".equals(state)) { connected = 0; configured = 0; } else if ("CONNECTED".equals(state)) { connected = 1; configured = 0; } else if ("CONFIGURED".equals(state)) { connected = 1; configured = 1; } else { Slog.e(TAG, "unknown state " + state); return; } removeMessages(MSG_UPDATE_STATE); Message msg = Message.obtain(this, MSG_UPDATE_STATE); msg.arg1 = connected; msg.arg2 = configured; // debounce disconnects to avoid problems bringing up USB tethering sendMessageDelayed(msg, (connected == 0) ? UPDATE_DELAY : 0); } private boolean waitForState(String state) { // wait for the transition to complete. // give up after 1 second. for (int i = 0; i < 20; i++) { // State transition is done when sys.usb.state is set to the new configuration if (state.equals(SystemProperties.get("sys.usb.state"))) return true; try { // try again in 50ms Thread.sleep(50); } catch (InterruptedException e) { } } Slog.e(TAG, "waitForState(" + state + ") FAILED"); return false; } private boolean setUsbConfig(String config) { if (DEBUG) Slog.d(TAG, "setUsbConfig(" + config + ")"); // set the new configuration SystemProperties.set("sys.usb.config", config); return waitForState(config); } private void setAdbEnabled(boolean enable) { if (DEBUG) Slog.d(TAG, "setAdbEnabled: " + enable); if (enable != mAdbEnabled) { mAdbEnabled = enable; // Due to the persist.sys.usb.config property trigger, changing adb state requires // switching to default function setEnabledFunctions(mDefaultFunctions, true); updateAdbNotification(); } SystemProperties.set("persist.service.adb.enable", enable ? "1":"0"); } private void setEnabledFunctions(String functions, boolean makeDefault) { if (functions != null && makeDefault) { if (mAdbEnabled) { functions = addFunction(functions, UsbManager.USB_FUNCTION_ADB); } else { functions = removeFunction(functions, UsbManager.USB_FUNCTION_ADB); } if (!mDefaultFunctions.equals(functions)) { if (!setUsbConfig("none")) { Slog.e(TAG, "Failed to disable USB"); // revert to previous configuration if we fail setUsbConfig(mCurrentFunctions); return; } // setting this property will also change the current USB state // via a property trigger SystemProperties.set("persist.sys.usb.config", functions); if (waitForState(functions)) { mCurrentFunctions = functions; mDefaultFunctions = functions; } else { Slog.e(TAG, "Failed to switch persistent USB config to " + functions); // revert to previous configuration if we fail SystemProperties.set("persist.sys.usb.config", mDefaultFunctions); } } } else { if (functions == null) { functions = mDefaultFunctions; } if (mAdbEnabled) { functions = addFunction(functions, UsbManager.USB_FUNCTION_ADB); } else { functions = removeFunction(functions, UsbManager.USB_FUNCTION_ADB); } if (!mCurrentFunctions.equals(functions)) { if (!setUsbConfig("none")) { Slog.e(TAG, "Failed to disable USB"); // revert to previous configuration if we fail setUsbConfig(mCurrentFunctions); return; } if (setUsbConfig(functions)) { mCurrentFunctions = functions; } else { Slog.e(TAG, "Failed to switch USB config to " + functions); // revert to previous configuration if we fail setUsbConfig(mCurrentFunctions); } } } } private void updateUsbState() { // send a sticky broadcast containing current USB state Intent intent = new Intent(UsbManager.ACTION_USB_STATE); intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); intent.putExtra(UsbManager.USB_CONNECTED, mConnected); intent.putExtra(UsbManager.USB_CONFIGURED, mConfigured); if (mCurrentFunctions != null) { String[] functions = mCurrentFunctions.split(","); for (int i = 0; i < functions.length; i++) { intent.putExtra(functions[i], true); } } mContext.sendStickyBroadcast(intent); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_STATE: if (DEBUG) Slog.d(TAG, "Got MSG_UPDATE_STATE. Connected="+msg.arg1+" Configured="+msg.arg2); mConnected = (msg.arg1 == 1); mConfigured = (msg.arg2 == 1); updateUsbNotification(); updateAdbNotification(); if (!mConnected) { // restore defaults when USB is disconnected setEnabledFunctions(mDefaultFunctions, false); } if (mBootCompleted) { updateUsbState(); } break; case MSG_ENABLE_ADB: setAdbEnabled(msg.arg1 == 1); break; case MSG_SET_CURRENT_FUNCTION: String function = (String)msg.obj; boolean makeDefault = (msg.arg1 == 1); setEnabledFunctions(function, makeDefault); break; case MSG_SYSTEM_READY: updateUsbNotification(); updateAdbNotification(); updateUsbState(); break; case MSG_BOOT_COMPLETED: mBootCompleted = true; if (mCurrentAccessory != null) { mSettingsManager.accessoryAttached(mCurrentAccessory); } break; } } public UsbAccessory getCurrentAccessory() { return mCurrentAccessory; } private void updateUsbNotification() { if (mNotificationManager == null || !mUseUsbNotification) { if(DEBUG && mNotificationManager == null) Slog.d(TAG, "mNotificationManager == null"); if(DEBUG && !mUseUsbNotification) Slog.d(TAG, "!mUseUsbNotification"); return; } int id = 0; Resources r = mContext.getResources(); if (mConnected) { if (containsFunction(mCurrentFunctions, UsbManager.USB_FUNCTION_MTP)) { id = com.android.internal.R.string.usb_mtp_notification_title; } else if (containsFunction(mCurrentFunctions, UsbManager.USB_FUNCTION_PTP)) { id = com.android.internal.R.string.usb_ptp_notification_title; } else if (containsFunction(mCurrentFunctions, UsbManager.USB_FUNCTION_MASS_STORAGE)) { id = com.android.internal.R.string.usb_cd_installer_notification_title; } else if (containsFunction(mCurrentFunctions, UsbManager.USB_FUNCTION_ACCESSORY)) { id = com.android.internal.R.string.usb_accessory_notification_title; } else { // There is a different notification for USB tethering so we don't need one here if (!containsFunction(mCurrentFunctions, UsbManager.USB_FUNCTION_RNDIS)) { Slog.e(TAG, "No known USB function in updateUsbNotification"); } } } if (id != mUsbNotificationId) { // clear notification if title needs changing if (mUsbNotificationId != 0) { mNotificationManager.cancel(mUsbNotificationId); mUsbNotificationId = 0; } if (id != 0) { CharSequence message = r.getText( com.android.internal.R.string.usb_notification_message); CharSequence title = r.getText(id); Notification notification = new Notification(); notification.icon = com.android.internal.R.drawable.stat_sys_data_usb; notification.when = 0; notification.flags = Notification.FLAG_ONGOING_EVENT; notification.tickerText = title; notification.defaults = 0; // please be quiet notification.sound = null; notification.vibrate = null; Intent intent = Intent.makeRestartActivityTask( new ComponentName("com.android.settings", "com.android.settings.UsbSettings")); PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); notification.setLatestEventInfo(mContext, title, message, pi); mNotificationManager.notify(id, notification); mUsbNotificationId = id; } } } private void updateAdbNotification() { if (mNotificationManager == null) return; final int id = com.android.internal.R.string.adb_active_notification_title; if (mAdbEnabled && mConnected) { if ("0".equals(SystemProperties.get("persist.adb.notify")) || Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.ADB_NOTIFY, 1) == 0) return; if (!mAdbNotificationShown) { Resources r = mContext.getResources(); CharSequence title = r.getText(id); CharSequence message = r.getText( com.android.internal.R.string.adb_active_notification_message); Notification notification = new Notification(); notification.icon = com.android.internal.R.drawable.stat_sys_adb; notification.when = 0; notification.flags = Notification.FLAG_ONGOING_EVENT; notification.tickerText = title; notification.defaults = 0; // please be quiet notification.sound = null; notification.vibrate = null; Intent intent = Intent.makeRestartActivityTask( new ComponentName("com.android.settings", "com.android.settings.DevelopmentSettings")); PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); notification.setLatestEventInfo(mContext, title, message, pi); mAdbNotificationShown = true; mNotificationManager.notify(id, notification); } } else if (mAdbNotificationShown) { mAdbNotificationShown = false; mNotificationManager.cancel(id); } } public void dump(FileDescriptor fd, PrintWriter pw) { pw.println(" USB Device State:"); pw.println(" Current Functions: " + mCurrentFunctions); pw.println(" Default Functions: " + mDefaultFunctions); pw.println(" mConnected: " + mConnected); pw.println(" mConfigured: " + mConfigured); pw.println(" mCurrentAccessory: " + mCurrentAccessory); try { pw.println(" Kernel function list: " + Arrays.toString(new File(FUNCTIONS_PATH).list())); pw.println(" Mass storage backing file: " + FileUtils.readTextFile(new File(MASS_STORAGE_FILE_PATH), 0, null).trim()); } catch (IOException e) { pw.println("IOException: " + e); } } } }