1 /* 2 * Copyright (C) 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.usb; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.res.Resources; 25 import android.os.Environment; 26 import android.os.Handler; 27 import android.os.HandlerThread; 28 import android.os.UserHandle; 29 import android.os.storage.StorageEventListener; 30 import android.os.storage.StorageManager; 31 import android.provider.Settings; 32 import android.util.Log; 33 34 import com.android.systemui.SystemUI; 35 36 public class StorageNotification extends SystemUI { 37 private static final String TAG = "StorageNotification"; 38 private static final boolean DEBUG = false; 39 40 private static final boolean POP_UMS_ACTIVITY_ON_CONNECT = true; 41 42 /** 43 * The notification that is shown when a USB mass storage host 44 * is connected. 45 * <p> 46 * This is lazily created, so use {@link #setUsbStorageNotification()}. 47 */ 48 private Notification mUsbStorageNotification; 49 50 /** 51 * The notification that is shown when the following media events occur: 52 * - Media is being checked 53 * - Media is blank (or unknown filesystem) 54 * - Media is corrupt 55 * - Media is safe to unmount 56 * - Media is missing 57 * <p> 58 * This is lazily created, so use {@link #setMediaStorageNotification()}. 59 */ 60 private Notification mMediaStorageNotification; 61 private boolean mUmsAvailable; 62 private StorageManager mStorageManager; 63 64 private Handler mAsyncEventHandler; 65 66 private class StorageNotificationEventListener extends StorageEventListener { onUsbMassStorageConnectionChanged(final boolean connected)67 public void onUsbMassStorageConnectionChanged(final boolean connected) { 68 mAsyncEventHandler.post(new Runnable() { 69 @Override 70 public void run() { 71 onUsbMassStorageConnectionChangedAsync(connected); 72 } 73 }); 74 } onStorageStateChanged(final String path, final String oldState, final String newState)75 public void onStorageStateChanged(final String path, 76 final String oldState, final String newState) { 77 mAsyncEventHandler.post(new Runnable() { 78 @Override 79 public void run() { 80 onStorageStateChangedAsync(path, oldState, newState); 81 } 82 }); 83 } 84 } 85 86 @Override start()87 public void start() { 88 mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); 89 final boolean connected = mStorageManager.isUsbMassStorageConnected(); 90 if (DEBUG) Log.d(TAG, String.format( "Startup with UMS connection %s (media state %s)", 91 mUmsAvailable, Environment.getExternalStorageState())); 92 93 HandlerThread thr = new HandlerThread("SystemUI StorageNotification"); 94 thr.start(); 95 mAsyncEventHandler = new Handler(thr.getLooper()); 96 97 StorageNotificationEventListener listener = new StorageNotificationEventListener(); 98 listener.onUsbMassStorageConnectionChanged(connected); 99 mStorageManager.registerListener(listener); 100 } 101 onUsbMassStorageConnectionChangedAsync(boolean connected)102 private void onUsbMassStorageConnectionChangedAsync(boolean connected) { 103 mUmsAvailable = connected; 104 /* 105 * Even though we may have a UMS host connected, we the SD card 106 * may not be in a state for export. 107 */ 108 String st = Environment.getExternalStorageState(); 109 110 if (DEBUG) Log.i(TAG, String.format("UMS connection changed to %s (media state %s)", 111 connected, st)); 112 113 if (connected && (st.equals( 114 Environment.MEDIA_REMOVED) || st.equals(Environment.MEDIA_CHECKING))) { 115 /* 116 * No card or card being checked = don't display 117 */ 118 connected = false; 119 } 120 updateUsbMassStorageNotification(connected); 121 } 122 onStorageStateChangedAsync(String path, String oldState, String newState)123 private void onStorageStateChangedAsync(String path, String oldState, String newState) { 124 if (DEBUG) Log.i(TAG, String.format( 125 "Media {%s} state changed from {%s} -> {%s}", path, oldState, newState)); 126 if (newState.equals(Environment.MEDIA_SHARED)) { 127 /* 128 * Storage is now shared. Modify the UMS notification 129 * for stopping UMS. 130 */ 131 Intent intent = new Intent(); 132 intent.setClass(mContext, com.android.systemui.usb.UsbStorageActivity.class); 133 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 134 setUsbStorageNotification( 135 com.android.internal.R.string.usb_storage_stop_notification_title, 136 com.android.internal.R.string.usb_storage_stop_notification_message, 137 com.android.internal.R.drawable.stat_sys_warning, false, true, pi); 138 } else if (newState.equals(Environment.MEDIA_CHECKING)) { 139 /* 140 * Storage is now checking. Update media notification and disable 141 * UMS notification. 142 */ 143 setMediaStorageNotification( 144 com.android.internal.R.string.ext_media_checking_notification_title, 145 com.android.internal.R.string.ext_media_checking_notification_message, 146 com.android.internal.R.drawable.stat_notify_sdcard_prepare, true, false, null); 147 updateUsbMassStorageNotification(false); 148 } else if (newState.equals(Environment.MEDIA_MOUNTED)) { 149 /* 150 * Storage is now mounted. Dismiss any media notifications, 151 * and enable UMS notification if connected. 152 */ 153 setMediaStorageNotification(0, 0, 0, false, false, null); 154 updateUsbMassStorageNotification(mUmsAvailable); 155 } else if (newState.equals(Environment.MEDIA_UNMOUNTED)) { 156 /* 157 * Storage is now unmounted. We may have been unmounted 158 * because the user is enabling/disabling UMS, in which case we don't 159 * want to display the 'safe to unmount' notification. 160 */ 161 if (!mStorageManager.isUsbMassStorageEnabled()) { 162 if (oldState.equals(Environment.MEDIA_SHARED)) { 163 /* 164 * The unmount was due to UMS being enabled. Dismiss any 165 * media notifications, and enable UMS notification if connected 166 */ 167 setMediaStorageNotification(0, 0, 0, false, false, null); 168 updateUsbMassStorageNotification(mUmsAvailable); 169 } else { 170 /* 171 * Show safe to unmount media notification, and enable UMS 172 * notification if connected. 173 */ 174 if (Environment.isExternalStorageRemovable()) { 175 setMediaStorageNotification( 176 com.android.internal.R.string.ext_media_safe_unmount_notification_title, 177 com.android.internal.R.string.ext_media_safe_unmount_notification_message, 178 com.android.internal.R.drawable.stat_notify_sdcard, true, true, null); 179 } else { 180 // This device does not have removable storage, so 181 // don't tell the user they can remove it. 182 setMediaStorageNotification(0, 0, 0, false, false, null); 183 } 184 updateUsbMassStorageNotification(mUmsAvailable); 185 } 186 } else { 187 /* 188 * The unmount was due to UMS being enabled. Dismiss any 189 * media notifications, and disable the UMS notification 190 */ 191 setMediaStorageNotification(0, 0, 0, false, false, null); 192 updateUsbMassStorageNotification(false); 193 } 194 } else if (newState.equals(Environment.MEDIA_NOFS)) { 195 /* 196 * Storage has no filesystem. Show blank media notification, 197 * and enable UMS notification if connected. 198 */ 199 Intent intent = new Intent(); 200 intent.setClass(mContext, com.android.internal.app.ExternalMediaFormatActivity.class); 201 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 202 203 setMediaStorageNotification( 204 com.android.internal.R.string.ext_media_nofs_notification_title, 205 com.android.internal.R.string.ext_media_nofs_notification_message, 206 com.android.internal.R.drawable.stat_notify_sdcard_usb, true, false, pi); 207 updateUsbMassStorageNotification(mUmsAvailable); 208 } else if (newState.equals(Environment.MEDIA_UNMOUNTABLE)) { 209 /* 210 * Storage is corrupt. Show corrupt media notification, 211 * and enable UMS notification if connected. 212 */ 213 Intent intent = new Intent(); 214 intent.setClass(mContext, com.android.internal.app.ExternalMediaFormatActivity.class); 215 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 216 217 setMediaStorageNotification( 218 com.android.internal.R.string.ext_media_unmountable_notification_title, 219 com.android.internal.R.string.ext_media_unmountable_notification_message, 220 com.android.internal.R.drawable.stat_notify_sdcard_usb, true, false, pi); 221 updateUsbMassStorageNotification(mUmsAvailable); 222 } else if (newState.equals(Environment.MEDIA_REMOVED)) { 223 /* 224 * Storage has been removed. Show nomedia media notification, 225 * and disable UMS notification regardless of connection state. 226 */ 227 setMediaStorageNotification( 228 com.android.internal.R.string.ext_media_nomedia_notification_title, 229 com.android.internal.R.string.ext_media_nomedia_notification_message, 230 com.android.internal.R.drawable.stat_notify_sdcard_usb, 231 true, false, null); 232 updateUsbMassStorageNotification(false); 233 } else if (newState.equals(Environment.MEDIA_BAD_REMOVAL)) { 234 /* 235 * Storage has been removed unsafely. Show bad removal media notification, 236 * and disable UMS notification regardless of connection state. 237 */ 238 setMediaStorageNotification( 239 com.android.internal.R.string.ext_media_badremoval_notification_title, 240 com.android.internal.R.string.ext_media_badremoval_notification_message, 241 com.android.internal.R.drawable.stat_sys_warning, 242 true, true, null); 243 updateUsbMassStorageNotification(false); 244 } else { 245 Log.w(TAG, String.format("Ignoring unknown state {%s}", newState)); 246 } 247 } 248 249 /** 250 * Update the state of the USB mass storage notification 251 */ updateUsbMassStorageNotification(boolean available)252 void updateUsbMassStorageNotification(boolean available) { 253 254 if (available) { 255 Intent intent = new Intent(); 256 intent.setClass(mContext, com.android.systemui.usb.UsbStorageActivity.class); 257 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 258 259 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 260 setUsbStorageNotification( 261 com.android.internal.R.string.usb_storage_notification_title, 262 com.android.internal.R.string.usb_storage_notification_message, 263 com.android.internal.R.drawable.stat_sys_data_usb, 264 false, true, pi); 265 } else { 266 setUsbStorageNotification(0, 0, 0, false, false, null); 267 } 268 } 269 270 /** 271 * Sets the USB storage notification. 272 */ setUsbStorageNotification(int titleId, int messageId, int icon, boolean sound, boolean visible, PendingIntent pi)273 private synchronized void setUsbStorageNotification(int titleId, int messageId, int icon, 274 boolean sound, boolean visible, PendingIntent pi) { 275 276 if (!visible && mUsbStorageNotification == null) { 277 return; 278 } 279 280 NotificationManager notificationManager = (NotificationManager) mContext 281 .getSystemService(Context.NOTIFICATION_SERVICE); 282 283 if (notificationManager == null) { 284 return; 285 } 286 287 if (visible) { 288 Resources r = Resources.getSystem(); 289 CharSequence title = r.getText(titleId); 290 CharSequence message = r.getText(messageId); 291 292 if (mUsbStorageNotification == null) { 293 mUsbStorageNotification = new Notification(); 294 mUsbStorageNotification.icon = icon; 295 mUsbStorageNotification.when = 0; 296 } 297 298 if (sound) { 299 mUsbStorageNotification.defaults |= Notification.DEFAULT_SOUND; 300 } else { 301 mUsbStorageNotification.defaults &= ~Notification.DEFAULT_SOUND; 302 } 303 304 mUsbStorageNotification.flags = Notification.FLAG_ONGOING_EVENT; 305 306 mUsbStorageNotification.tickerText = title; 307 if (pi == null) { 308 Intent intent = new Intent(); 309 pi = PendingIntent.getBroadcastAsUser(mContext, 0, intent, 0, 310 UserHandle.CURRENT); 311 } 312 mUsbStorageNotification.color = mContext.getResources().getColor( 313 com.android.internal.R.color.system_notification_accent_color); 314 mUsbStorageNotification.setLatestEventInfo(mContext, title, message, pi); 315 mUsbStorageNotification.visibility = Notification.VISIBILITY_PUBLIC; 316 mUsbStorageNotification.category = Notification.CATEGORY_SYSTEM; 317 318 final boolean adbOn = 1 == Settings.Global.getInt( 319 mContext.getContentResolver(), 320 Settings.Global.ADB_ENABLED, 321 0); 322 323 if (POP_UMS_ACTIVITY_ON_CONNECT && !adbOn) { 324 // Pop up a full-screen alert to coach the user through enabling UMS. The average 325 // user has attached the device to USB either to charge the phone (in which case 326 // this is harmless) or transfer files, and in the latter case this alert saves 327 // several steps (as well as subtly indicates that you shouldn't mix UMS with other 328 // activities on the device). 329 // 330 // If ADB is enabled, however, we suppress this dialog (under the assumption that a 331 // developer (a) knows how to enable UMS, and (b) is probably using USB to install 332 // builds or use adb commands. 333 mUsbStorageNotification.fullScreenIntent = pi; 334 } 335 } 336 337 final int notificationId = mUsbStorageNotification.icon; 338 if (visible) { 339 notificationManager.notifyAsUser(null, notificationId, mUsbStorageNotification, 340 UserHandle.ALL); 341 } else { 342 notificationManager.cancelAsUser(null, notificationId, UserHandle.ALL); 343 } 344 } 345 getMediaStorageNotificationDismissable()346 private synchronized boolean getMediaStorageNotificationDismissable() { 347 if ((mMediaStorageNotification != null) && 348 ((mMediaStorageNotification.flags & Notification.FLAG_AUTO_CANCEL) == 349 Notification.FLAG_AUTO_CANCEL)) 350 return true; 351 352 return false; 353 } 354 355 /** 356 * Sets the media storage notification. 357 */ setMediaStorageNotification(int titleId, int messageId, int icon, boolean visible, boolean dismissable, PendingIntent pi)358 private synchronized void setMediaStorageNotification(int titleId, int messageId, int icon, boolean visible, 359 boolean dismissable, PendingIntent pi) { 360 361 if (!visible && mMediaStorageNotification == null) { 362 return; 363 } 364 365 NotificationManager notificationManager = (NotificationManager) mContext 366 .getSystemService(Context.NOTIFICATION_SERVICE); 367 368 if (notificationManager == null) { 369 return; 370 } 371 372 if (mMediaStorageNotification != null && visible) { 373 /* 374 * Dismiss the previous notification - we're about to 375 * re-use it. 376 */ 377 final int notificationId = mMediaStorageNotification.icon; 378 notificationManager.cancel(notificationId); 379 } 380 381 if (visible) { 382 Resources r = Resources.getSystem(); 383 CharSequence title = r.getText(titleId); 384 CharSequence message = r.getText(messageId); 385 386 if (mMediaStorageNotification == null) { 387 mMediaStorageNotification = new Notification(); 388 mMediaStorageNotification.when = 0; 389 } 390 391 mMediaStorageNotification.defaults &= ~Notification.DEFAULT_SOUND; 392 393 if (dismissable) { 394 mMediaStorageNotification.flags = Notification.FLAG_AUTO_CANCEL; 395 } else { 396 mMediaStorageNotification.flags = Notification.FLAG_ONGOING_EVENT; 397 } 398 399 mMediaStorageNotification.tickerText = title; 400 if (pi == null) { 401 Intent intent = new Intent(); 402 pi = PendingIntent.getBroadcastAsUser(mContext, 0, intent, 0, 403 UserHandle.CURRENT); 404 } 405 406 mMediaStorageNotification.icon = icon; 407 mMediaStorageNotification.color = mContext.getResources().getColor( 408 com.android.internal.R.color.system_notification_accent_color); 409 mMediaStorageNotification.setLatestEventInfo(mContext, title, message, pi); 410 mMediaStorageNotification.visibility = Notification.VISIBILITY_PUBLIC; 411 mMediaStorageNotification.category = Notification.CATEGORY_SYSTEM; 412 } 413 414 final int notificationId = mMediaStorageNotification.icon; 415 if (visible) { 416 notificationManager.notifyAsUser(null, notificationId, 417 mMediaStorageNotification, UserHandle.ALL); 418 } else { 419 notificationManager.cancelAsUser(null, notificationId, UserHandle.ALL); 420 } 421 } 422 } 423