Home | History | Annotate | Download | only in cellbroadcastreceiver
      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 
     17 package com.android.cellbroadcastreceiver;
     18 
     19 import android.app.KeyguardManager;
     20 import android.app.Notification;
     21 import android.app.NotificationManager;
     22 import android.app.PendingIntent;
     23 import android.app.Service;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.SharedPreferences;
     27 import android.os.Bundle;
     28 import android.os.IBinder;
     29 import android.os.UserHandle;
     30 import android.preference.PreferenceManager;
     31 import android.provider.Telephony;
     32 import android.telephony.CellBroadcastMessage;
     33 import android.telephony.SmsCbCmasInfo;
     34 import android.telephony.SmsCbLocation;
     35 import android.telephony.SmsCbMessage;
     36 import android.util.Log;
     37 
     38 import java.util.ArrayList;
     39 import 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  */
     47 public 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