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