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