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