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