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.ActivityManagerNative;
     20 import android.app.KeyguardManager;
     21 import android.app.Notification;
     22 import android.app.NotificationManager;
     23 import android.app.PendingIntent;
     24 import android.app.Service;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.SharedPreferences;
     28 import android.os.Bundle;
     29 import android.os.IBinder;
     30 import android.os.RemoteException;
     31 import android.os.UserHandle;
     32 import android.preference.PreferenceManager;
     33 import android.provider.Telephony;
     34 import android.telephony.CarrierConfigManager;
     35 import android.telephony.CellBroadcastMessage;
     36 import android.telephony.SmsCbCmasInfo;
     37 import android.telephony.SmsCbEtwsInfo;
     38 import android.telephony.SmsCbLocation;
     39 import android.telephony.SmsCbMessage;
     40 import android.util.Log;
     41 
     42 import com.android.cellbroadcastreceiver.CellBroadcastAlertAudio.ToneType;
     43 import com.android.cellbroadcastreceiver.CellBroadcastOtherChannelsManager.CellBroadcastChannelRange;
     44 
     45 import java.util.ArrayList;
     46 import java.util.HashSet;
     47 import java.util.Locale;
     48 
     49 /**
     50  * This service manages the display and animation of broadcast messages.
     51  * Emergency messages display with a flashing animated exclamation mark icon,
     52  * and an alert tone is played when the alert is first shown to the user
     53  * (but not when the user views a previously received broadcast).
     54  */
     55 public class CellBroadcastAlertService extends Service {
     56     private static final String TAG = "CBAlertService";
     57 
     58     /** Intent action to display alert dialog/notification, after verifying the alert is new. */
     59     static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
     60 
     61     /** Use the same notification ID for non-emergency alerts. */
     62     static final int NOTIFICATION_ID = 1;
     63 
     64     /** Sticky broadcast for latest area info broadcast received. */
     65     static final String CB_AREA_INFO_RECEIVED_ACTION =
     66             "android.cellbroadcastreceiver.CB_AREA_INFO_RECEIVED";
     67 
     68     /**
     69      *  Container for service category, serial number, location, body hash code, and ETWS primary/
     70      *  secondary information for duplication detection.
     71      */
     72     private static final class MessageServiceCategoryAndScope {
     73         private final int mServiceCategory;
     74         private final int mSerialNumber;
     75         private final SmsCbLocation mLocation;
     76         private final int mBodyHash;
     77         private final boolean mIsEtwsPrimary;
     78 
     79         MessageServiceCategoryAndScope(int serviceCategory, int serialNumber,
     80                 SmsCbLocation location, int bodyHash, boolean isEtwsPrimary) {
     81             mServiceCategory = serviceCategory;
     82             mSerialNumber = serialNumber;
     83             mLocation = location;
     84             mBodyHash = bodyHash;
     85             mIsEtwsPrimary = isEtwsPrimary;
     86         }
     87 
     88         @Override
     89         public int hashCode() {
     90             return mLocation.hashCode() + 5 * mServiceCategory + 7 * mSerialNumber + 13 * mBodyHash
     91                     + 17 * Boolean.hashCode(mIsEtwsPrimary);
     92         }
     93 
     94         @Override
     95         public boolean equals(Object o) {
     96             if (o == this) {
     97                 return true;
     98             }
     99             if (o instanceof MessageServiceCategoryAndScope) {
    100                 MessageServiceCategoryAndScope other = (MessageServiceCategoryAndScope) o;
    101                 return (mServiceCategory == other.mServiceCategory &&
    102                         mSerialNumber == other.mSerialNumber &&
    103                         mLocation.equals(other.mLocation) &&
    104                         mBodyHash == other.mBodyHash &&
    105                         mIsEtwsPrimary == other.mIsEtwsPrimary);
    106             }
    107             return false;
    108         }
    109 
    110         @Override
    111         public String toString() {
    112             return "{mServiceCategory: " + mServiceCategory + " serial number: " + mSerialNumber +
    113                     " location: " + mLocation.toString() + " body hash: " + mBodyHash +
    114                     " mIsEtwsPrimary: " + mIsEtwsPrimary + "}";
    115         }
    116     }
    117 
    118     /** Cache of received message IDs, for duplicate message detection. */
    119     private static final HashSet<MessageServiceCategoryAndScope> sCmasIdSet =
    120             new HashSet<MessageServiceCategoryAndScope>(8);
    121 
    122     /** Maximum number of message IDs to save before removing the oldest message ID. */
    123     private static final int MAX_MESSAGE_ID_SIZE = 65535;
    124 
    125     /** List of message IDs received, for removing oldest ID when max message IDs are received. */
    126     private static final ArrayList<MessageServiceCategoryAndScope> sCmasIdList =
    127             new ArrayList<MessageServiceCategoryAndScope>(8);
    128 
    129     /** Index of message ID to replace with new message ID when max message IDs are received. */
    130     private static int sCmasIdListIndex = 0;
    131 
    132     @Override
    133     public int onStartCommand(Intent intent, int flags, int startId) {
    134         String action = intent.getAction();
    135         if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) ||
    136                 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
    137             handleCellBroadcastIntent(intent);
    138         } else if (SHOW_NEW_ALERT_ACTION.equals(action)) {
    139             try {
    140                 if (UserHandle.myUserId() ==
    141                         ActivityManagerNative.getDefault().getCurrentUser().id) {
    142                     showNewAlert(intent);
    143                 } else {
    144                     Log.d(TAG,"Not active user, ignore the alert display");
    145                 }
    146             } catch (RemoteException e) {
    147                 e.printStackTrace();
    148             }
    149         } else {
    150             Log.e(TAG, "Unrecognized intent action: " + action);
    151         }
    152         return START_NOT_STICKY;
    153     }
    154 
    155     private void handleCellBroadcastIntent(Intent intent) {
    156         Bundle extras = intent.getExtras();
    157         if (extras == null) {
    158             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
    159             return;
    160         }
    161 
    162         SmsCbMessage message = (SmsCbMessage) extras.get("message");
    163 
    164         if (message == null) {
    165             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
    166             return;
    167         }
    168 
    169         final CellBroadcastMessage cbm = new CellBroadcastMessage(message);
    170 
    171         if (!isMessageEnabledByUser(cbm)) {
    172             Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() +
    173                     " by user preference");
    174             return;
    175         }
    176 
    177         // If this is an ETWS message, then we want to include the body message to be a factor for
    178         // duplication detection. We found that some Japanese carriers send ETWS messages
    179         // with the same serial number, therefore the subsequent messages were all ignored.
    180         // In the other hand, US carriers have the requirement that only serial number, location,
    181         // and category should be used for duplicate detection.
    182         int hashCode = message.isEtwsMessage() ? message.getMessageBody().hashCode() : 0;
    183 
    184         // If this is an ETWS message, we need to include primary/secondary message information to
    185         // be a factor for duplication detection as well. Per 3GPP TS 23.041 section 8.2,
    186         // duplicate message detection shall be performed independently for primary and secondary
    187         // notifications.
    188         boolean isEtwsPrimary = false;
    189         if (message.isEtwsMessage()) {
    190             SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo();
    191             if (etwsInfo != null) {
    192                 isEtwsPrimary = etwsInfo.isPrimary();
    193             } else {
    194                 Log.w(TAG, "ETWS info is not available.");
    195             }
    196         }
    197 
    198         // Check for duplicate message IDs according to CMAS carrier requirements. Message IDs
    199         // are stored in volatile memory. If the maximum of 65535 messages is reached, the
    200         // message ID of the oldest message is deleted from the list.
    201         MessageServiceCategoryAndScope newCmasId = new MessageServiceCategoryAndScope(
    202                 message.getServiceCategory(), message.getSerialNumber(), message.getLocation(),
    203                 hashCode, isEtwsPrimary);
    204 
    205         Log.d(TAG, "message ID = " + newCmasId);
    206 
    207         // Add the new message ID to the list. It's okay if this is a duplicate message ID,
    208         // because the list is only used for removing old message IDs from the hash set.
    209         if (sCmasIdList.size() < MAX_MESSAGE_ID_SIZE) {
    210             sCmasIdList.add(newCmasId);
    211         } else {
    212             // Get oldest message ID from the list and replace with the new message ID.
    213             MessageServiceCategoryAndScope oldestCmasId = sCmasIdList.get(sCmasIdListIndex);
    214             sCmasIdList.set(sCmasIdListIndex, newCmasId);
    215             Log.d(TAG, "message ID limit reached, removing oldest message ID " + oldestCmasId);
    216             // Remove oldest message ID from the set.
    217             sCmasIdSet.remove(oldestCmasId);
    218             if (++sCmasIdListIndex >= MAX_MESSAGE_ID_SIZE) {
    219                 sCmasIdListIndex = 0;
    220             }
    221         }
    222         // Set.add() returns false if message ID has already been added
    223         if (!sCmasIdSet.add(newCmasId)) {
    224             Log.d(TAG, "ignoring duplicate alert with " + newCmasId);
    225             return;
    226         }
    227 
    228         final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
    229         alertIntent.setClass(this, CellBroadcastAlertService.class);
    230         alertIntent.putExtra("message", cbm);
    231 
    232         // write to database on a background thread
    233         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
    234                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
    235                     @Override
    236                     public boolean execute(CellBroadcastContentProvider provider) {
    237                         if (provider.insertNewBroadcast(cbm)) {
    238                             // new message, show the alert or notification on UI thread
    239                             startService(alertIntent);
    240                             return true;
    241                         } else {
    242                             return false;
    243                         }
    244                     }
    245                 });
    246     }
    247 
    248     private void showNewAlert(Intent intent) {
    249         Bundle extras = intent.getExtras();
    250         if (extras == null) {
    251             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
    252             return;
    253         }
    254 
    255         CellBroadcastMessage cbm = (CellBroadcastMessage) intent.getParcelableExtra("message");
    256 
    257         if (cbm == null) {
    258             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
    259             return;
    260         }
    261 
    262         if (cbm.isEmergencyAlertMessage()) {
    263             // start alert sound / vibration / TTS and display full-screen alert
    264             openEmergencyAlertNotification(cbm);
    265         } else {
    266             // add notification to the bar by passing the list of unread non-emergency
    267             // CellBroadcastMessages
    268             ArrayList<CellBroadcastMessage> messageList = CellBroadcastReceiverApp
    269                     .addNewMessageToList(cbm);
    270             addToNotificationBar(cbm, messageList, this, false);
    271         }
    272     }
    273 
    274     /**
    275      * Filter out broadcasts on the test channels that the user has not enabled,
    276      * and types of notifications that the user is not interested in receiving.
    277      * This allows us to enable an entire range of message identifiers in the
    278      * radio and not have to explicitly disable the message identifiers for
    279      * test broadcasts. In the unlikely event that the default shared preference
    280      * values were not initialized in CellBroadcastReceiverApp, the second parameter
    281      * to the getBoolean() calls match the default values in res/xml/preferences.xml.
    282      *
    283      * @param message the message to check
    284      * @return true if the user has enabled this message type; false otherwise
    285      */
    286     private boolean isMessageEnabledByUser(CellBroadcastMessage message) {
    287 
    288         // Check if all emergency alerts are disabled.
    289         boolean emergencyAlertEnabled = PreferenceManager.getDefaultSharedPreferences(this).
    290                 getBoolean(CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS, true);
    291 
    292         // Check if ETWS/CMAS test message is forced to disabled on the device.
    293         boolean forceDisableEtwsCmasTest =
    294                 CellBroadcastSettings.isFeatureEnabled(this,
    295                         CarrierConfigManager.KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false);
    296 
    297         if (message.isEtwsTestMessage()) {
    298             return emergencyAlertEnabled &&
    299                     !forceDisableEtwsCmasTest &&
    300                     PreferenceManager.getDefaultSharedPreferences(this)
    301                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false);
    302         }
    303 
    304         if (message.isEtwsMessage()) {
    305             // ETWS messages.
    306             // Turn on/off emergency notifications is the only way to turn on/off ETWS messages.
    307             return emergencyAlertEnabled;
    308 
    309         }
    310 
    311         if (message.isCmasMessage()) {
    312             switch (message.getCmasMessageClass()) {
    313                 case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
    314                     return emergencyAlertEnabled &&
    315                             PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    316                             CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
    317 
    318                 case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
    319                     return emergencyAlertEnabled &&
    320                             PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    321                             CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
    322 
    323                 case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
    324                     return emergencyAlertEnabled &&
    325                             PreferenceManager.getDefaultSharedPreferences(this)
    326                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
    327 
    328                 case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
    329                 case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
    330                 case SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE:
    331                     return emergencyAlertEnabled &&
    332                             !forceDisableEtwsCmasTest &&
    333                             PreferenceManager.getDefaultSharedPreferences(this)
    334                                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS,
    335                                             false);
    336                 default:
    337                     return true;    // presidential-level CMAS alerts are always enabled
    338             }
    339         }
    340 
    341         if (message.getServiceCategory() == 50) {
    342             // save latest area info broadcast for Settings display and send as broadcast
    343             CellBroadcastReceiverApp.setLatestAreaInfo(message);
    344             Intent intent = new Intent(CB_AREA_INFO_RECEIVED_ACTION);
    345             intent.putExtra("message", message);
    346             // Send broadcast twice, once for apps that have PRIVILEGED permission and once
    347             // for those that have the runtime one
    348             sendBroadcastAsUser(intent, UserHandle.ALL,
    349                     android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
    350             sendBroadcastAsUser(intent, UserHandle.ALL,
    351                     android.Manifest.permission.READ_PHONE_STATE);
    352             return false;   // area info broadcasts are displayed in Settings status screen
    353         }
    354 
    355         return true;    // other broadcast messages are always enabled
    356     }
    357 
    358     /**
    359      * Display a full-screen alert message for emergency alerts.
    360      * @param message the alert to display
    361      */
    362     private void openEmergencyAlertNotification(CellBroadcastMessage message) {
    363         // Acquire a CPU wake lock until the alert dialog and audio start playing.
    364         CellBroadcastAlertWakeLock.acquireScreenCpuWakeLock(this);
    365 
    366         // Close dialogs and window shade
    367         Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
    368         sendBroadcast(closeDialogs);
    369 
    370         // start audio/vibration/speech service for emergency alerts
    371         Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
    372         audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
    373         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    374 
    375         ToneType toneType = ToneType.CMAS_DEFAULT;
    376         if (message.isEtwsMessage()) {
    377             // For ETWS, always vibrate, even in silent mode.
    378             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA, true);
    379             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_ETWS_VIBRATE_EXTRA, true);
    380             toneType = ToneType.ETWS_DEFAULT;
    381 
    382             if (message.getEtwsWarningInfo() != null) {
    383                 int warningType = message.getEtwsWarningInfo().getWarningType();
    384 
    385                 switch (warningType) {
    386                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE:
    387                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
    388                         toneType = ToneType.EARTHQUAKE;
    389                         break;
    390                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI:
    391                         toneType = ToneType.TSUNAMI;
    392                         break;
    393                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY:
    394                         toneType = ToneType.OTHER;
    395                         break;
    396                 }
    397             }
    398         } else {
    399             // For other alerts, vibration can be disabled in app settings.
    400             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA,
    401                     prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true));
    402             int channel = message.getServiceCategory();
    403             ArrayList<CellBroadcastChannelRange> ranges= CellBroadcastOtherChannelsManager.
    404                     getInstance().getCellBroadcastChannelRanges(getApplicationContext(),
    405                     message.getSubId());
    406             if (ranges != null) {
    407                 for (CellBroadcastChannelRange range : ranges) {
    408                     if (channel >= range.mStartId && channel <= range.mEndId) {
    409                         toneType = range.mToneType;
    410                         break;
    411                     }
    412                 }
    413             }
    414         }
    415         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE, toneType);
    416 
    417         String messageBody = message.getMessageBody();
    418 
    419         if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) {
    420             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
    421 
    422             String preferredLanguage = message.getLanguageCode();
    423             String defaultLanguage = null;
    424             if (message.isEtwsMessage()) {
    425                 // Only do TTS for ETWS secondary message.
    426                 // There is no text in ETWS primary message. When we construct the ETWS primary
    427                 // message, we hardcode "ETWS" as the body hence we don't want to speak that out
    428                 // here.
    429 
    430                 // Also in many cases we see the secondary message comes few milliseconds after
    431                 // the primary one. If we play TTS for the primary one, It will be overwritten by
    432                 // the secondary one immediately anyway.
    433                 if (!message.getEtwsWarningInfo().isPrimary()) {
    434                     // Since only Japanese carriers are using ETWS, if there is no language
    435                     // specified in the ETWS message, we'll use Japanese as the default language.
    436                     defaultLanguage = "ja";
    437                 }
    438             } else {
    439                 // If there is no language specified in the CMAS message, use device's
    440                 // default language.
    441                 defaultLanguage = Locale.getDefault().getLanguage();
    442             }
    443 
    444             Log.d(TAG, "Preferred language = " + preferredLanguage +
    445                     ", Default language = " + defaultLanguage);
    446             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE,
    447                     preferredLanguage);
    448             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE,
    449                     defaultLanguage);
    450         }
    451         startService(audioIntent);
    452 
    453         // Decide which activity to start based on the state of the keyguard.
    454         Class c = CellBroadcastAlertDialog.class;
    455         KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
    456         if (km.inKeyguardRestrictedInputMode()) {
    457             // Use the full screen activity for security.
    458             c = CellBroadcastAlertFullScreen.class;
    459         }
    460 
    461         ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1);
    462         messageList.add(message);
    463 
    464         Intent alertDialogIntent = createDisplayMessageIntent(this, c, messageList);
    465         alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    466         startActivity(alertDialogIntent);
    467     }
    468 
    469     /**
    470      * Add the new alert to the notification bar (non-emergency alerts), or launch a
    471      * high-priority immediate intent for emergency alerts.
    472      * @param message the alert to display
    473      */
    474     static void addToNotificationBar(CellBroadcastMessage message,
    475                                      ArrayList<CellBroadcastMessage> messageList, Context context,
    476                                      boolean fromSaveState) {
    477         int channelTitleId = CellBroadcastResources.getDialogTitleResource(message);
    478         CharSequence channelName = context.getText(channelTitleId);
    479         String messageBody = message.getMessageBody();
    480 
    481         // Create intent to show the new messages when user selects the notification.
    482         Intent intent = createDisplayMessageIntent(context, CellBroadcastAlertDialog.class,
    483                 messageList);
    484         intent.putExtra(CellBroadcastAlertFullScreen.FROM_NOTIFICATION_EXTRA, true);
    485         intent.putExtra(CellBroadcastAlertFullScreen.FROM_SAVE_STATE_NOTIFICATION_EXTRA,
    486                 fromSaveState);
    487 
    488         PendingIntent pi = PendingIntent.getActivity(context, NOTIFICATION_ID, intent,
    489                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    490 
    491         // use default sound/vibration/lights for non-emergency broadcasts
    492         Notification.Builder builder = new Notification.Builder(context)
    493                 .setSmallIcon(R.drawable.ic_notify_alert)
    494                 .setTicker(channelName)
    495                 .setWhen(System.currentTimeMillis())
    496                 .setContentIntent(pi)
    497                 .setCategory(Notification.CATEGORY_SYSTEM)
    498                 .setPriority(Notification.PRIORITY_HIGH)
    499                 .setColor(context.getResources().getColor(R.color.notification_color))
    500                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    501                 .setDefaults(Notification.DEFAULT_ALL);
    502 
    503         builder.setDefaults(Notification.DEFAULT_ALL);
    504 
    505         // increment unread alert count (decremented when user dismisses alert dialog)
    506         int unreadCount = messageList.size();
    507         if (unreadCount > 1) {
    508             // use generic count of unread broadcasts if more than one unread
    509             builder.setContentTitle(context.getString(R.string.notification_multiple_title));
    510             builder.setContentText(context.getString(R.string.notification_multiple, unreadCount));
    511         } else {
    512             builder.setContentTitle(channelName).setContentText(messageBody);
    513         }
    514 
    515         NotificationManager notificationManager = NotificationManager.from(context);
    516 
    517         notificationManager.notify(NOTIFICATION_ID, builder.build());
    518     }
    519 
    520     static Intent createDisplayMessageIntent(Context context, Class intentClass,
    521             ArrayList<CellBroadcastMessage> messageList) {
    522         // Trigger the list activity to fire up a dialog that shows the received messages
    523         Intent intent = new Intent(context, intentClass);
    524         intent.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, messageList);
    525         return intent;
    526     }
    527 
    528     @Override
    529     public IBinder onBind(Intent intent) {
    530         return null;    // clients can't bind to this service
    531     }
    532 }
    533