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