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