CellBroadcastAlertService.java revision 0224babc0bec527dd7e44babb2cd644836f8e1fb
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.app.ActivityManagerNative;
25import android.content.Context;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.os.Bundle;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.os.UserHandle;
32import android.preference.PreferenceManager;
33import android.provider.Telephony;
34import android.telephony.CellBroadcastMessage;
35import android.telephony.SmsCbCmasInfo;
36import android.telephony.SmsCbLocation;
37import android.telephony.SmsCbMessage;
38import android.util.Log;
39
40import java.util.ArrayList;
41import java.util.HashSet;
42
43/**
44 * This service manages the display and animation of broadcast messages.
45 * Emergency messages display with a flashing animated exclamation mark icon,
46 * and an alert tone is played when the alert is first shown to the user
47 * (but not when the user views a previously received broadcast).
48 */
49public class CellBroadcastAlertService extends Service {
50    private static final String TAG = "CellBroadcastAlertService";
51
52    /** Intent action to display alert dialog/notification, after verifying the alert is new. */
53    static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
54
55    /** Use the same notification ID for non-emergency alerts. */
56    static final int NOTIFICATION_ID = 1;
57
58    /** Sticky broadcast for latest area info broadcast received. */
59    static final String CB_AREA_INFO_RECEIVED_ACTION =
60            "android.cellbroadcastreceiver.CB_AREA_INFO_RECEIVED";
61
62    /** Container for message ID and geographical scope, for duplicate message detection. */
63    private static final class MessageServiceCategoryAndScope {
64        private final int mServiceCategory;
65        private final int mSerialNumber;
66        private final SmsCbLocation mLocation;
67
68        MessageServiceCategoryAndScope(int serviceCategory, int serialNumber,
69                SmsCbLocation location) {
70            mServiceCategory = serviceCategory;
71            mSerialNumber = serialNumber;
72            mLocation = location;
73        }
74
75        @Override
76        public int hashCode() {
77            return mLocation.hashCode() + 5 * mServiceCategory + 7 * mSerialNumber;
78        }
79
80        @Override
81        public boolean equals(Object o) {
82            if (o == this) {
83                return true;
84            }
85            if (o instanceof MessageServiceCategoryAndScope) {
86                MessageServiceCategoryAndScope other = (MessageServiceCategoryAndScope) o;
87                return (mServiceCategory == other.mServiceCategory &&
88                        mSerialNumber == other.mSerialNumber &&
89                        mLocation.equals(other.mLocation));
90            }
91            return false;
92        }
93
94        @Override
95        public String toString() {
96            return "{mServiceCategory: " + mServiceCategory + " serial number: " + mSerialNumber +
97                    " location: " + mLocation.toString() + '}';
98        }
99    }
100
101    /** Cache of received message IDs, for duplicate message detection. */
102    private static final HashSet<MessageServiceCategoryAndScope> sCmasIdSet =
103            new HashSet<MessageServiceCategoryAndScope>(8);
104
105    /** Maximum number of message IDs to save before removing the oldest message ID. */
106    private static final int MAX_MESSAGE_ID_SIZE = 65535;
107
108    /** List of message IDs received, for removing oldest ID when max message IDs are received. */
109    private static final ArrayList<MessageServiceCategoryAndScope> sCmasIdList =
110            new ArrayList<MessageServiceCategoryAndScope>(8);
111
112    /** Index of message ID to replace with new message ID when max message IDs are received. */
113    private static int sCmasIdListIndex = 0;
114
115    @Override
116    public int onStartCommand(Intent intent, int flags, int startId) {
117        String action = intent.getAction();
118        if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) ||
119                Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
120            handleCellBroadcastIntent(intent);
121        } else if (SHOW_NEW_ALERT_ACTION.equals(action)) {
122            try {
123                if (UserHandle.myUserId() ==
124                        ActivityManagerNative.getDefault().getCurrentUser().id) {
125                    showNewAlert(intent);
126                } else {
127                    Log.d(TAG,"Not active user, ignore the alert display");
128                }
129            } catch (RemoteException e) {
130                e.printStackTrace();
131            }
132        } else {
133            Log.e(TAG, "Unrecognized intent action: " + action);
134        }
135        return START_NOT_STICKY;
136    }
137
138    private void handleCellBroadcastIntent(Intent intent) {
139        Bundle extras = intent.getExtras();
140        if (extras == null) {
141            Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
142            return;
143        }
144
145        SmsCbMessage message = (SmsCbMessage) extras.get("message");
146
147        if (message == null) {
148            Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
149            return;
150        }
151
152        final CellBroadcastMessage cbm = new CellBroadcastMessage(message);
153        if (!isMessageEnabledByUser(cbm)) {
154            Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() +
155                    " by user preference");
156            return;
157        }
158
159        // Check for duplicate message IDs according to CMAS carrier requirements. Message IDs
160        // are stored in volatile memory. If the maximum of 65535 messages is reached, the
161        // message ID of the oldest message is deleted from the list.
162        MessageServiceCategoryAndScope newCmasId = new MessageServiceCategoryAndScope(
163                message.getServiceCategory(), message.getSerialNumber(), message.getLocation());
164
165        // Add the new message ID to the list. It's okay if this is a duplicate message ID,
166        // because the list is only used for removing old message IDs from the hash set.
167        if (sCmasIdList.size() < MAX_MESSAGE_ID_SIZE) {
168            sCmasIdList.add(newCmasId);
169        } else {
170            // Get oldest message ID from the list and replace with the new message ID.
171            MessageServiceCategoryAndScope oldestCmasId = sCmasIdList.get(sCmasIdListIndex);
172            sCmasIdList.set(sCmasIdListIndex, newCmasId);
173            Log.d(TAG, "message ID limit reached, removing oldest message ID " + oldestCmasId);
174            // Remove oldest message ID from the set.
175            sCmasIdSet.remove(oldestCmasId);
176            if (++sCmasIdListIndex >= MAX_MESSAGE_ID_SIZE) {
177                sCmasIdListIndex = 0;
178            }
179        }
180        // Set.add() returns false if message ID has already been added
181        if (!sCmasIdSet.add(newCmasId)) {
182            Log.d(TAG, "ignoring duplicate alert with " + newCmasId);
183            return;
184        }
185
186        final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
187        alertIntent.setClass(this, CellBroadcastAlertService.class);
188        alertIntent.putExtra("message", cbm);
189
190        // write to database on a background thread
191        new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
192                .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
193                    @Override
194                    public boolean execute(CellBroadcastContentProvider provider) {
195                        if (provider.insertNewBroadcast(cbm)) {
196                            // new message, show the alert or notification on UI thread
197                            startService(alertIntent);
198                            return true;
199                        } else {
200                            return false;
201                        }
202                    }
203                });
204    }
205
206    private void showNewAlert(Intent intent) {
207        Bundle extras = intent.getExtras();
208        if (extras == null) {
209            Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
210            return;
211        }
212
213        CellBroadcastMessage cbm = (CellBroadcastMessage) extras.get("message");
214
215        if (cbm == null) {
216            Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
217            return;
218        }
219
220        if (CellBroadcastConfigService.isEmergencyAlertMessage(cbm)) {
221            // start alert sound / vibration / TTS and display full-screen alert
222            openEmergencyAlertNotification(cbm);
223        } else {
224            // add notification to the bar
225            addToNotificationBar(cbm);
226        }
227    }
228
229    /**
230     * Filter out broadcasts on the test channels that the user has not enabled,
231     * and types of notifications that the user is not interested in receiving.
232     * This allows us to enable an entire range of message identifiers in the
233     * radio and not have to explicitly disable the message identifiers for
234     * test broadcasts. In the unlikely event that the default shared preference
235     * values were not initialized in CellBroadcastReceiverApp, the second parameter
236     * to the getBoolean() calls match the default values in res/xml/preferences.xml.
237     *
238     * @param message the message to check
239     * @return true if the user has enabled this message type; false otherwise
240     */
241    private boolean isMessageEnabledByUser(CellBroadcastMessage message) {
242        if (message.isEtwsTestMessage()) {
243            return PreferenceManager.getDefaultSharedPreferences(this)
244                    .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false);
245        }
246
247        if (message.isCmasMessage()) {
248            switch (message.getCmasMessageClass()) {
249                case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
250                    return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
251                            CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
252
253                case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
254                    return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
255                            CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
256
257                case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
258                    return PreferenceManager.getDefaultSharedPreferences(this)
259                            .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
260
261                case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
262                case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
263                case SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE:
264                    return PreferenceManager.getDefaultSharedPreferences(this)
265                            .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, false);
266
267                default:
268                    return true;    // presidential-level CMAS alerts are always enabled
269            }
270        }
271
272        if (message.getServiceCategory() == 50) {
273            // save latest area info broadcast for Settings display and send as broadcast
274            CellBroadcastReceiverApp.setLatestAreaInfo(message);
275            Intent intent = new Intent(CB_AREA_INFO_RECEIVED_ACTION);
276            intent.putExtra("message", message);
277            sendBroadcastAsUser(intent, UserHandle.ALL,
278                    android.Manifest.permission.READ_PHONE_STATE);
279            return false;   // area info broadcasts are displayed in Settings status screen
280        }
281
282        return true;    // other broadcast messages are always enabled
283    }
284
285    /**
286     * Display a full-screen alert message for emergency alerts.
287     * @param message the alert to display
288     */
289    private void openEmergencyAlertNotification(CellBroadcastMessage message) {
290        // Acquire a CPU wake lock until the alert dialog and audio start playing.
291        CellBroadcastAlertWakeLock.acquireScreenCpuWakeLock(this);
292
293        // Close dialogs and window shade
294        Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
295        sendBroadcast(closeDialogs);
296
297        // start audio/vibration/speech service for emergency alerts
298        Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
299        audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
300        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
301
302        int duration;   // alert audio duration in ms
303        if (message.isCmasMessage()) {
304            // CMAS requirement: duration of the audio attention signal is 10.5 seconds.
305            duration = 10500;
306        } else {
307            duration = Integer.parseInt(prefs.getString(
308                    CellBroadcastSettings.KEY_ALERT_SOUND_DURATION,
309                    CellBroadcastSettings.ALERT_SOUND_DEFAULT_DURATION)) * 1000;
310        }
311        audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION_EXTRA, duration);
312
313        if (message.isEtwsMessage()) {
314            // For ETWS, always vibrate, even in silent mode.
315            audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA, true);
316            audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_ETWS_VIBRATE_EXTRA, true);
317        } else {
318            // For other alerts, vibration can be disabled in app settings.
319            audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA,
320                    prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true));
321        }
322
323        String messageBody = message.getMessageBody();
324
325        if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) {
326            audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
327
328            String language = message.getLanguageCode();
329            if (message.isEtwsMessage() && !"ja".equals(language)) {
330                Log.w(TAG, "bad language code for ETWS - using Japanese TTS");
331                language = "ja";
332            } else if (message.isCmasMessage() && !"en".equals(language)) {
333                Log.w(TAG, "bad language code for CMAS - using English TTS");
334                language = "en";
335            }
336            audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE,
337                    language);
338        }
339        startService(audioIntent);
340
341        // Decide which activity to start based on the state of the keyguard.
342        Class c = CellBroadcastAlertDialog.class;
343        KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
344        if (km.inKeyguardRestrictedInputMode()) {
345            // Use the full screen activity for security.
346            c = CellBroadcastAlertFullScreen.class;
347        }
348
349        ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1);
350        messageList.add(message);
351
352        Intent alertDialogIntent = createDisplayMessageIntent(this, c, messageList);
353        alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
354        startActivity(alertDialogIntent);
355    }
356
357    /**
358     * Add the new alert to the notification bar (non-emergency alerts), or launch a
359     * high-priority immediate intent for emergency alerts.
360     * @param message the alert to display
361     */
362    private void addToNotificationBar(CellBroadcastMessage message) {
363        int channelTitleId = CellBroadcastResources.getDialogTitleResource(message);
364        CharSequence channelName = getText(channelTitleId);
365        String messageBody = message.getMessageBody();
366
367        // Pass the list of unread non-emergency CellBroadcastMessages
368        ArrayList<CellBroadcastMessage> messageList = CellBroadcastReceiverApp
369                .addNewMessageToList(message);
370
371        // Create intent to show the new messages when user selects the notification.
372        Intent intent = createDisplayMessageIntent(this, CellBroadcastAlertDialog.class,
373                messageList);
374        intent.putExtra(CellBroadcastAlertFullScreen.FROM_NOTIFICATION_EXTRA, true);
375
376        PendingIntent pi = PendingIntent.getActivity(this, NOTIFICATION_ID, intent,
377                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
378
379        // use default sound/vibration/lights for non-emergency broadcasts
380        Notification.Builder builder = new Notification.Builder(this)
381                .setSmallIcon(R.drawable.ic_notify_alert)
382                .setTicker(channelName)
383                .setWhen(System.currentTimeMillis())
384                .setContentIntent(pi)
385                .setCategory(Notification.CATEGORY_SYSTEM)
386                .setPriority(Notification.PRIORITY_HIGH)
387                .setColor(getResources().getColor(R.color.notification_color))
388                .setVisibility(Notification.VISIBILITY_PUBLIC)
389                .setDefaults(Notification.DEFAULT_ALL);
390
391        builder.setDefaults(Notification.DEFAULT_ALL);
392
393        // increment unread alert count (decremented when user dismisses alert dialog)
394        int unreadCount = messageList.size();
395        if (unreadCount > 1) {
396            // use generic count of unread broadcasts if more than one unread
397            builder.setContentTitle(getString(R.string.notification_multiple_title));
398            builder.setContentText(getString(R.string.notification_multiple, unreadCount));
399        } else {
400            builder.setContentTitle(channelName).setContentText(messageBody);
401        }
402
403        NotificationManager notificationManager =
404            (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
405
406        notificationManager.notify(NOTIFICATION_ID, builder.build());
407    }
408
409    static Intent createDisplayMessageIntent(Context context, Class intentClass,
410            ArrayList<CellBroadcastMessage> messageList) {
411        // Trigger the list activity to fire up a dialog that shows the received messages
412        Intent intent = new Intent(context, intentClass);
413        intent.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, messageList);
414        return intent;
415    }
416
417    @Override
418    public IBinder onBind(Intent intent) {
419        return null;    // clients can't bind to this service
420    }
421}
422