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