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