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