CellBroadcastAlertService.java revision eef14be1b2b77fc08a6cc5ef301ba49ea54c0c0a
1/*
2 * Copyright (C) 2011 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.cellbroadcastreceiver;
18
19import android.app.KeyguardManager;
20import android.app.Notification;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.app.Service;
24import android.content.Context;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.os.Bundle;
28import android.os.IBinder;
29import android.os.PowerManager;
30import android.preference.PreferenceManager;
31import android.provider.Telephony;
32import android.telephony.CellBroadcastMessage;
33import android.telephony.SmsCbCmasInfo;
34import android.telephony.SmsCbMessage;
35import android.util.Log;
36
37/**
38 * This service manages the display and animation of broadcast messages.
39 * Emergency messages display with a flashing animated exclamation mark icon,
40 * and an alert tone is played when the alert is first shown to the user
41 * (but not when the user views a previously received broadcast).
42 */
43public class CellBroadcastAlertService extends Service {
44    private static final String TAG = "CellBroadcastAlertService";
45
46    /** Identifier for notification ID extra. */
47    public static final String SMS_CB_NOTIFICATION_ID_EXTRA =
48            "com.android.cellbroadcastreceiver.SMS_CB_NOTIFICATION_ID";
49
50    /** Intent extra to indicate a previously unread alert. */
51    static final String NEW_ALERT_EXTRA = "com.android.cellbroadcastreceiver.NEW_ALERT";
52
53    /** Intent action to display alert dialog/notification, after verifying the alert is new. */
54    static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
55
56    /** Use the same notification ID for non-emergency alerts. */
57    static final int NOTIFICATION_ID = 1;
58
59    /** CPU wake lock while handling emergency alert notification. */
60    private PowerManager.WakeLock mWakeLock;
61
62    /** Hold the wake lock for 5 seconds, which should be enough time to display the alert. */
63    private static final int WAKE_LOCK_TIMEOUT = 5000;
64
65    @Override
66    public int onStartCommand(Intent intent, int flags, int startId) {
67        String action = intent.getAction();
68        if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) ||
69                Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
70            handleCellBroadcastIntent(intent);
71        } else if (SHOW_NEW_ALERT_ACTION.equals(action)) {
72            showNewAlert(intent);
73        } else {
74            Log.e(TAG, "Unrecognized intent action: " + action);
75        }
76        return START_NOT_STICKY;
77    }
78
79    private void handleCellBroadcastIntent(Intent intent) {
80        Bundle extras = intent.getExtras();
81        if (extras == null) {
82            Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
83            return;
84        }
85
86        SmsCbMessage message = (SmsCbMessage) extras.get("message");
87
88        if (message == null) {
89            Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
90            return;
91        }
92
93        final CellBroadcastMessage cbm = new CellBroadcastMessage(message);
94        if (!isMessageEnabledByUser(cbm)) {
95            Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() +
96                    " by user preference");
97            return;
98        }
99
100        final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
101        alertIntent.setClass(this, CellBroadcastAlertService.class);
102        alertIntent.putExtra("message", cbm);
103
104        // write to database on a background thread
105        new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
106                .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
107                    @Override
108                    public boolean execute(CellBroadcastContentProvider provider) {
109                        if (provider.insertNewBroadcast(cbm)) {
110                            // new message, show the alert or notification on UI thread
111                            startService(alertIntent);
112                            return true;
113                        } else {
114                            return false;
115                        }
116                    }
117                });
118    }
119
120    private void showNewAlert(Intent intent) {
121        Bundle extras = intent.getExtras();
122        if (extras == null) {
123            Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
124            return;
125        }
126
127        CellBroadcastMessage cbm = (CellBroadcastMessage) extras.get("message");
128
129        if (cbm == null) {
130            Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
131            return;
132        }
133
134        if (cbm.isEmergencyAlertMessage() || CellBroadcastConfigService
135                .isOperatorDefinedEmergencyId(cbm.getServiceCategory())) {
136            // start alert sound / vibration / TTS and display full-screen alert
137            openEmergencyAlertNotification(cbm);
138        } else {
139            // add notification to the bar
140            addToNotificationBar(cbm);
141        }
142    }
143
144    /**
145     * Filter out broadcasts on the test channels that the user has not enabled,
146     * and types of notifications that the user is not interested in receiving.
147     * This allows us to enable an entire range of message identifiers in the
148     * radio and not have to explicitly disable the message identifiers for
149     * test broadcasts. In the unlikely event that the default shared preference
150     * values were not initialized in CellBroadcastReceiverApp, the second parameter
151     * to the getBoolean() calls match the default values in res/xml/preferences.xml.
152     *
153     * @param message the message to check
154     * @return true if the user has enabled this message type; false otherwise
155     */
156    private boolean isMessageEnabledByUser(CellBroadcastMessage message) {
157        if (message.isEtwsTestMessage()) {
158            return PreferenceManager.getDefaultSharedPreferences(this)
159                    .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false);
160        }
161
162        if (message.isCmasMessage()) {
163            switch (message.getCmasMessageClass()) {
164                case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
165                    return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
166                            CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
167
168                case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
169                    return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
170                            CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
171
172                case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
173                    return PreferenceManager.getDefaultSharedPreferences(this)
174                            .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
175
176                case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
177                case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
178                    return PreferenceManager.getDefaultSharedPreferences(this)
179                            .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, false);
180
181                default:
182                    return true;    // presidential-level CMAS alerts are always enabled
183            }
184        }
185
186        return true;    // other broadcast messages are always enabled
187    }
188
189    private void acquireTimedWakelock(int timeout) {
190        if (mWakeLock == null) {
191            PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
192            // Note: acquiring a PARTIAL_WAKE_LOCK and setting window flag FLAG_TURN_SCREEN_ON in
193            // CellBroadcastAlertFullScreen is not sufficient to turn on the screen by itself.
194            // Use SCREEN_BRIGHT_WAKE_LOCK here as a workaround to ensure the screen turns on.
195            mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK
196                    | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG);
197        }
198        mWakeLock.acquire(timeout);
199    }
200
201    /**
202     * Display a full-screen alert message for emergency alerts.
203     * @param message the alert to display
204     */
205    private void openEmergencyAlertNotification(CellBroadcastMessage message) {
206        // Acquire a CPU wake lock until the alert dialog and audio start playing.
207        acquireTimedWakelock(WAKE_LOCK_TIMEOUT);
208
209        // Close dialogs and window shade
210        Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
211        sendBroadcast(closeDialogs);
212
213        // start audio/vibration/speech service for emergency alerts
214        Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
215        audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
216        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
217        String duration = prefs.getString(CellBroadcastSettings.KEY_ALERT_SOUND_DURATION,
218                CellBroadcastSettings.ALERT_SOUND_DEFAULT_DURATION);
219        audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION_EXTRA,
220                Integer.parseInt(duration));
221
222        int channelTitleId = CellBroadcastResources.getDialogTitleResource(message);
223        CharSequence channelName = getText(channelTitleId);
224        String messageBody = message.getMessageBody();
225
226        if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) {
227            audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
228
229            String language = message.getLanguageCode();
230            if (message.isEtwsMessage() && !"ja".equals(language)) {
231                Log.w(TAG, "bad language code for ETWS - using Japanese TTS");
232                language = "ja";
233            } else if (message.isCmasMessage() && !"en".equals(language)) {
234                Log.w(TAG, "bad language code for CMAS - using English TTS");
235                language = "en";
236            }
237            audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE,
238                    language);
239        }
240        startService(audioIntent);
241
242        // Use lower 32 bits of emergency alert delivery time for notification ID
243        int notificationId = (int) message.getDeliveryTime();
244
245        // Decide which activity to start based on the state of the keyguard.
246        Class c = CellBroadcastAlertDialog.class;
247        KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
248        if (km.inKeyguardRestrictedInputMode()) {
249            // Use the full screen activity for security.
250            c = CellBroadcastAlertFullScreen.class;
251        }
252
253        Intent notify = createDisplayMessageIntent(this, c, message, notificationId);
254        PendingIntent pi = PendingIntent.getActivity(this, notificationId, notify, 0);
255
256        Notification.Builder builder = new Notification.Builder(this)
257                .setSmallIcon(R.drawable.ic_notify_alert)
258                .setTicker(getText(CellBroadcastResources.getDialogTitleResource(message)))
259                .setWhen(System.currentTimeMillis())
260                .setContentIntent(pi)
261                .setFullScreenIntent(pi, true)
262                .setContentTitle(channelName)
263                .setContentText(messageBody)
264                .setDefaults(Notification.DEFAULT_LIGHTS);
265
266        NotificationManager notificationManager =
267            (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
268
269        notificationManager.notify(notificationId, builder.getNotification());
270    }
271
272    /**
273     * Add the new alert to the notification bar (non-emergency alerts), or launch a
274     * high-priority immediate intent for emergency alerts.
275     * @param message the alert to display
276     */
277    private void addToNotificationBar(CellBroadcastMessage message) {
278        int channelTitleId = CellBroadcastResources.getDialogTitleResource(message);
279        CharSequence channelName = getText(channelTitleId);
280        String messageBody = message.getMessageBody();
281
282        // Use the same ID to create a single notification for multiple non-emergency alerts.
283        int notificationId = NOTIFICATION_ID;
284
285        PendingIntent pi = PendingIntent.getActivity(this, 0, createDisplayMessageIntent(
286                this, CellBroadcastListActivity.class, message, notificationId), 0);
287
288        // use default sound/vibration/lights for non-emergency broadcasts
289        Notification.Builder builder = new Notification.Builder(this)
290                .setSmallIcon(R.drawable.ic_notify_alert)
291                .setTicker(channelName)
292                .setWhen(System.currentTimeMillis())
293                .setContentIntent(pi)
294                .setDefaults(Notification.DEFAULT_ALL);
295
296        builder.setDefaults(Notification.DEFAULT_ALL);
297
298        // increment unread alert count (decremented when user dismisses alert dialog)
299        int unreadCount = CellBroadcastReceiverApp.incrementUnreadAlertCount();
300        if (unreadCount > 1) {
301            // use generic count of unread broadcasts if more than one unread
302            builder.setContentTitle(getString(R.string.notification_multiple_title));
303            builder.setContentText(getString(R.string.notification_multiple, unreadCount));
304        } else {
305            builder.setContentTitle(channelName).setContentText(messageBody);
306        }
307
308        Log.i(TAG, "addToNotificationBar notificationId: " + notificationId);
309
310        NotificationManager notificationManager =
311            (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
312
313        notificationManager.notify(notificationId, builder.getNotification());
314    }
315
316    static Intent createDisplayMessageIntent(Context context, Class intentClass,
317            CellBroadcastMessage message, int notificationId) {
318        // Trigger the list activity to fire up a dialog that shows the received messages
319        Intent intent = new Intent(context, intentClass);
320        intent.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, message);
321        intent.putExtra(SMS_CB_NOTIFICATION_ID_EXTRA, notificationId);
322        intent.putExtra(NEW_ALERT_EXTRA, true);
323
324        // This line is needed to make this intent compare differently than the other intents
325        // created here for other messages. Without this line, the PendingIntent always gets the
326        // intent of a previous message and notification.
327        intent.setType(Integer.toString(notificationId));
328
329        return intent;
330    }
331
332    @Override
333    public IBinder onBind(Intent intent) {
334        return null;    // clients can't bind to this service
335    }
336}
337