MountService.java revision 3dec7d563a2f3e1eb967ce2054a00b6620e3558c
1/*
2 * Copyright (C) 2007 The Android Open Source Project
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.server;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.content.pm.PackageManager;
27import android.content.res.Resources;
28import android.net.Uri;
29import android.os.IMountService;
30import android.os.Environment;
31import android.os.RemoteException;
32import android.os.SystemProperties;
33import android.os.UEventObserver;
34import android.text.TextUtils;
35import android.util.Log;
36
37import java.io.File;
38import java.io.FileReader;
39
40/**
41 * MountService implements an to the mount service daemon
42 * @hide
43 */
44class MountService extends IMountService.Stub {
45
46    private static final String TAG = "MountService";
47
48    /**
49     * Binder context for this service
50     */
51    private Context mContext;
52
53    /**
54     * listener object for communicating with the mount service daemon
55     */
56    private MountListener mListener;
57
58    /**
59     * The notification that is shown when a USB mass storage host
60     * is connected.
61     * <p>
62     * This is lazily created, so use {@link #setUsbStorageNotification()}.
63     */
64    private Notification mUsbStorageNotification;
65
66
67    /**
68     * The notification that is shown when the following media events occur:
69     *     - Media is being checked
70     *     - Media is blank (or unknown filesystem)
71     *     - Media is corrupt
72     *     - Media is safe to unmount
73     *     - Media is missing
74     * <p>
75     * This is lazily created, so use {@link #setMediaStorageNotification()}.
76     */
77    private Notification mMediaStorageNotification;
78
79    private boolean mShowSafeUnmountNotificationWhenUnmounted;
80
81    private boolean mPlaySounds;
82
83    private boolean mMounted;
84
85    /**
86     * Constructs a new MountService instance
87     *
88     * @param context  Binder context for this service
89     */
90    public MountService(Context context) {
91        mContext = context;
92
93        // Register a BOOT_COMPLETED handler so that we can start
94        // MountListener. We defer the startup so that we don't
95        // start processing events before we ought-to
96        mContext.registerReceiver(mBroadcastReceiver,
97                new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null);
98
99        mListener =  new MountListener(this);
100        mShowSafeUnmountNotificationWhenUnmounted = false;
101
102        mPlaySounds = SystemProperties.get("persist.service.mount.playsnd", "1").equals("1");
103    }
104
105    BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
106        public void onReceive(Context context, Intent intent) {
107            if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
108                Thread thread = new Thread(mListener, MountListener.class.getName());
109                thread.start();
110            }
111        }
112    };
113
114    /**
115     * @return true if USB mass storage support is enabled.
116     */
117    public boolean getMassStorageEnabled() throws RemoteException {
118        return mListener.getMassStorageEnabled();
119    }
120
121    /**
122     * Enables or disables USB mass storage support.
123     *
124     * @param enable  true to enable USB mass storage support
125     */
126    public void setMassStorageEnabled(boolean enable) throws RemoteException {
127        mListener.setMassStorageEnabled(enable);
128    }
129
130    /**
131     * @return true if USB mass storage is connected.
132     */
133    public boolean getMassStorageConnected() throws RemoteException {
134        return mListener.getMassStorageConnected();
135    }
136
137    /**
138     * Attempt to mount external media
139     */
140    public void mountMedia(String mountPath) throws RemoteException {
141        if (mContext.checkCallingOrSelfPermission(
142                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
143                != PackageManager.PERMISSION_GRANTED) {
144            throw new SecurityException("Requires MOUNT_UNMOUNT_FILESYSTEMS permission");
145        }
146        mListener.mountMedia(mountPath);
147    }
148
149    /**
150     * Attempt to unmount external media to prepare for eject
151     */
152    public void unmountMedia(String mountPath) throws RemoteException {
153        if (mContext.checkCallingOrSelfPermission(
154                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
155                != PackageManager.PERMISSION_GRANTED) {
156            throw new SecurityException("Requires MOUNT_UNMOUNT_FILESYSTEMS permission");
157        }
158
159        // Set a flag so that when we get the unmounted event, we know
160        // to display the notification
161        mShowSafeUnmountNotificationWhenUnmounted = true;
162
163        // tell mountd to unmount the media
164        mListener.ejectMedia(mountPath);
165    }
166
167    /**
168     * Attempt to format external media
169     */
170    public void formatMedia(String formatPath) throws RemoteException {
171        if (mContext.checkCallingOrSelfPermission(
172                android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
173                != PackageManager.PERMISSION_GRANTED) {
174            throw new SecurityException("Requires MOUNT_FORMAT_FILESYSTEMS permission");
175        }
176
177        mListener.formatMedia(formatPath);
178    }
179
180    /**
181     * Returns true if we're playing media notification sounds.
182     */
183    public boolean getPlayNotificationSounds() {
184        return mPlaySounds;
185    }
186
187    /**
188     * Set whether or not we're playing media notification sounds.
189     */
190    public void setPlayNotificationSounds(boolean enabled) {
191        if (mContext.checkCallingOrSelfPermission(
192                android.Manifest.permission.WRITE_SETTINGS)
193                != PackageManager.PERMISSION_GRANTED) {
194            throw new SecurityException("Requires WRITE_SETTINGS permission");
195        }
196        mPlaySounds = enabled;
197        SystemProperties.set("persist.service.mount.playsnd", (enabled ? "1" : "0"));
198    }
199
200    /**
201     * Update the state of the USB mass storage notification
202     */
203    void updateUsbMassStorageNotification(boolean suppressIfConnected, boolean sound) {
204
205        try {
206
207            if (getMassStorageConnected() && !suppressIfConnected) {
208                Intent intent = new Intent();
209                intent.setClass(mContext, com.android.internal.app.UsbStorageActivity.class);
210                PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
211                setUsbStorageNotification(
212                        com.android.internal.R.string.usb_storage_notification_title,
213                        com.android.internal.R.string.usb_storage_notification_message,
214                        com.android.internal.R.drawable.stat_sys_data_usb,
215                        sound, true, pi);
216            } else {
217                setUsbStorageNotification(0, 0, 0, false, false, null);
218            }
219        } catch (RemoteException e) {
220            // Nothing to do
221        }
222    }
223
224    void handlePossibleExplicitUnmountBroadcast(String path) {
225        if (mMounted) {
226            mMounted = false;
227            Intent intent = new Intent(Intent.ACTION_MEDIA_UNMOUNTED,
228                    Uri.parse("file://" + path));
229            mContext.sendBroadcast(intent);
230        }
231    }
232
233    /**
234     * Broadcasts the USB mass storage connected event to all clients.
235     */
236    void notifyUmsConnected() {
237        String storageState = Environment.getExternalStorageState();
238        if (!storageState.equals(Environment.MEDIA_REMOVED) &&
239            !storageState.equals(Environment.MEDIA_BAD_REMOVAL) &&
240            !storageState.equals(Environment.MEDIA_CHECKING)) {
241
242            updateUsbMassStorageNotification(false, true);
243        }
244
245        Intent intent = new Intent(Intent.ACTION_UMS_CONNECTED);
246        mContext.sendBroadcast(intent);
247    }
248
249    /**
250     * Broadcasts the USB mass storage disconnected event to all clients.
251     */
252    void notifyUmsDisconnected() {
253        updateUsbMassStorageNotification(false, false);
254        Intent intent = new Intent(Intent.ACTION_UMS_DISCONNECTED);
255        mContext.sendBroadcast(intent);
256    }
257
258    /**
259     * Broadcasts the media removed event to all clients.
260     */
261    void notifyMediaRemoved(String path) {
262        updateUsbMassStorageNotification(true, false);
263
264        setMediaStorageNotification(
265                com.android.internal.R.string.ext_media_nomedia_notification_title,
266                com.android.internal.R.string.ext_media_nomedia_notification_message,
267                com.android.internal.R.drawable.stat_sys_no_sim,
268                true, false, null);
269        handlePossibleExplicitUnmountBroadcast(path);
270
271        Intent intent = new Intent(Intent.ACTION_MEDIA_REMOVED,
272                Uri.parse("file://" + path));
273        mContext.sendBroadcast(intent);
274    }
275
276    /**
277     * Broadcasts the media unmounted event to all clients.
278     */
279    void notifyMediaUnmounted(String path) {
280        if (mShowSafeUnmountNotificationWhenUnmounted) {
281            setMediaStorageNotification(
282                    com.android.internal.R.string.ext_media_safe_unmount_notification_title,
283                    com.android.internal.R.string.ext_media_safe_unmount_notification_message,
284                    com.android.internal.R.drawable.stat_notify_sim_toolkit,
285                    true, true, null);
286            mShowSafeUnmountNotificationWhenUnmounted = false;
287        } else {
288            setMediaStorageNotification(0, 0, 0, false, false, null);
289        }
290        updateUsbMassStorageNotification(false, false);
291
292        Intent intent = new Intent(Intent.ACTION_MEDIA_UNMOUNTED,
293                Uri.parse("file://" + path));
294        mContext.sendBroadcast(intent);
295    }
296
297    /**
298     * Broadcasts the media checking event to all clients.
299     */
300    void notifyMediaChecking(String path) {
301        setMediaStorageNotification(
302                com.android.internal.R.string.ext_media_checking_notification_title,
303                com.android.internal.R.string.ext_media_checking_notification_message,
304                com.android.internal.R.drawable.stat_notify_sim_toolkit,
305                true, false, null);
306
307        updateUsbMassStorageNotification(true, false);
308        Intent intent = new Intent(Intent.ACTION_MEDIA_CHECKING,
309                Uri.parse("file://" + path));
310        mContext.sendBroadcast(intent);
311    }
312
313    /**
314     * Broadcasts the media nofs event to all clients.
315     */
316    void notifyMediaNoFs(String path) {
317
318        Intent intent = new Intent();
319        intent.setClass(mContext, com.android.internal.app.ExternalMediaFormatActivity.class);
320        PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
321
322        setMediaStorageNotification(com.android.internal.R.string.ext_media_nofs_notification_title,
323                                    com.android.internal.R.string.ext_media_nofs_notification_message,
324                                    com.android.internal.R.drawable.stat_sys_no_sim,
325                                    true, false, pi);
326        updateUsbMassStorageNotification(false, false);
327        intent = new Intent(Intent.ACTION_MEDIA_NOFS,
328                Uri.parse("file://" + path));
329        mContext.sendBroadcast(intent);
330    }
331
332    /**
333     * Broadcasts the media mounted event to all clients.
334     */
335    void notifyMediaMounted(String path, boolean readOnly) {
336        setMediaStorageNotification(0, 0, 0, false, false, null);
337        updateUsbMassStorageNotification(false, false);
338        Intent intent = new Intent(Intent.ACTION_MEDIA_MOUNTED,
339                Uri.parse("file://" + path));
340        intent.putExtra("read-only", readOnly);
341        mMounted = true;
342        mContext.sendBroadcast(intent);
343    }
344
345    /**
346     * Broadcasts the media shared event to all clients.
347     */
348    void notifyMediaShared(String path) {
349        Intent intent = new Intent();
350        intent.setClass(mContext, com.android.internal.app.UsbStorageStopActivity.class);
351        PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
352        setUsbStorageNotification(com.android.internal.R.string.usb_storage_stop_notification_title,
353                                  com.android.internal.R.string.usb_storage_stop_notification_message,
354                                  com.android.internal.R.drawable.stat_sys_warning,
355                                  false, true, pi);
356        handlePossibleExplicitUnmountBroadcast(path);
357        intent = new Intent(Intent.ACTION_MEDIA_SHARED,
358                Uri.parse("file://" + path));
359        mContext.sendBroadcast(intent);
360    }
361
362    /**
363     * Broadcasts the media bad removal event to all clients.
364     */
365    void notifyMediaBadRemoval(String path) {
366        updateUsbMassStorageNotification(true, false);
367        setMediaStorageNotification(com.android.internal.R.string.ext_media_badremoval_notification_title,
368                                    com.android.internal.R.string.ext_media_badremoval_notification_message,
369                                    com.android.internal.R.drawable.stat_sys_warning,
370                                    true, true, null);
371
372        handlePossibleExplicitUnmountBroadcast(path);
373        Intent intent = new Intent(Intent.ACTION_MEDIA_BAD_REMOVAL,
374                Uri.parse("file://" + path));
375        mContext.sendBroadcast(intent);
376
377        intent = new Intent(Intent.ACTION_MEDIA_REMOVED,
378                Uri.parse("file://" + path));
379        mContext.sendBroadcast(intent);
380    }
381
382    /**
383     * Broadcasts the media unmountable event to all clients.
384     */
385    void notifyMediaUnmountable(String path) {
386        Intent intent = new Intent();
387        intent.setClass(mContext, com.android.internal.app.ExternalMediaFormatActivity.class);
388        PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
389
390        setMediaStorageNotification(com.android.internal.R.string.ext_media_unmountable_notification_title,
391                                    com.android.internal.R.string.ext_media_unmountable_notification_message,
392                                    com.android.internal.R.drawable.stat_sys_no_sim,
393                                    true, false, pi);
394        updateUsbMassStorageNotification(false, false);
395
396        handlePossibleExplicitUnmountBroadcast(path);
397
398        intent = new Intent(Intent.ACTION_MEDIA_UNMOUNTABLE,
399                Uri.parse("file://" + path));
400        mContext.sendBroadcast(intent);
401    }
402
403    /**
404     * Broadcasts the media eject event to all clients.
405     */
406    void notifyMediaEject(String path) {
407        Intent intent = new Intent(Intent.ACTION_MEDIA_EJECT,
408                Uri.parse("file://" + path));
409        mContext.sendBroadcast(intent);
410    }
411
412    /**
413     * Sets the USB storage notification.
414     */
415    private synchronized void setUsbStorageNotification(int titleId, int messageId, int icon, boolean sound, boolean visible,
416                                                        PendingIntent pi) {
417
418        if (!visible && mUsbStorageNotification == null) {
419            return;
420        }
421
422        NotificationManager notificationManager = (NotificationManager) mContext
423                .getSystemService(Context.NOTIFICATION_SERVICE);
424
425        if (notificationManager == null) {
426            return;
427        }
428
429        if (visible) {
430            Resources r = Resources.getSystem();
431            CharSequence title = r.getText(titleId);
432            CharSequence message = r.getText(messageId);
433
434            if (mUsbStorageNotification == null) {
435                mUsbStorageNotification = new Notification();
436                mUsbStorageNotification.icon = icon;
437                mUsbStorageNotification.when = 0;
438            }
439
440            if (sound && mPlaySounds) {
441                mUsbStorageNotification.defaults |= Notification.DEFAULT_SOUND;
442            } else {
443                mUsbStorageNotification.defaults &= ~Notification.DEFAULT_SOUND;
444            }
445
446            mUsbStorageNotification.flags = Notification.FLAG_ONGOING_EVENT;
447
448            mUsbStorageNotification.tickerText = title;
449            if (pi == null) {
450                Intent intent = new Intent();
451                pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
452            }
453
454            mUsbStorageNotification.setLatestEventInfo(mContext, title, message, pi);
455        }
456
457        final int notificationId = mUsbStorageNotification.icon;
458        if (visible) {
459            notificationManager.notify(notificationId, mUsbStorageNotification);
460        } else {
461            notificationManager.cancel(notificationId);
462        }
463    }
464
465    private synchronized boolean getMediaStorageNotificationDismissable() {
466        if ((mMediaStorageNotification != null) &&
467            ((mMediaStorageNotification.flags & Notification.FLAG_AUTO_CANCEL) ==
468                    Notification.FLAG_AUTO_CANCEL))
469            return true;
470
471        return false;
472    }
473
474    /**
475     * Sets the media storage notification.
476     */
477    private synchronized void setMediaStorageNotification(int titleId, int messageId, int icon, boolean visible,
478                                                          boolean dismissable, PendingIntent pi) {
479
480        if (!visible && mMediaStorageNotification == null) {
481            return;
482        }
483
484        NotificationManager notificationManager = (NotificationManager) mContext
485                .getSystemService(Context.NOTIFICATION_SERVICE);
486
487        if (notificationManager == null) {
488            return;
489        }
490
491        if (mMediaStorageNotification != null && visible) {
492            /*
493             * Dismiss the previous notification - we're about to
494             * re-use it.
495             */
496            final int notificationId = mMediaStorageNotification.icon;
497            notificationManager.cancel(notificationId);
498        }
499
500        if (visible) {
501            Resources r = Resources.getSystem();
502            CharSequence title = r.getText(titleId);
503            CharSequence message = r.getText(messageId);
504
505            if (mMediaStorageNotification == null) {
506                mMediaStorageNotification = new Notification();
507                mMediaStorageNotification.when = 0;
508            }
509
510            if (mPlaySounds) {
511                mMediaStorageNotification.defaults |= Notification.DEFAULT_SOUND;
512            } else {
513                mMediaStorageNotification.defaults &= ~Notification.DEFAULT_SOUND;
514            }
515
516            if (dismissable) {
517                mMediaStorageNotification.flags = Notification.FLAG_AUTO_CANCEL;
518            } else {
519                mMediaStorageNotification.flags = Notification.FLAG_ONGOING_EVENT;
520            }
521
522            mMediaStorageNotification.tickerText = title;
523            if (pi == null) {
524                Intent intent = new Intent();
525                pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
526            }
527
528            mMediaStorageNotification.icon = icon;
529            mMediaStorageNotification.setLatestEventInfo(mContext, title, message, pi);
530        }
531
532        final int notificationId = mMediaStorageNotification.icon;
533        if (visible) {
534            notificationManager.notify(notificationId, mMediaStorageNotification);
535        } else {
536            notificationManager.cancel(notificationId);
537        }
538    }
539}
540
541