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