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 static android.text.format.DateUtils.DAY_IN_MILLIS;
     20 
     21 import android.app.ActivityManager;
     22 import android.app.Notification;
     23 import android.app.NotificationChannel;
     24 import android.app.NotificationManager;
     25 import android.app.PendingIntent;
     26 import android.app.Service;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.SharedPreferences;
     30 import android.content.pm.PackageManager;
     31 import android.content.res.Resources;
     32 import android.os.Binder;
     33 import android.os.Bundle;
     34 import android.os.IBinder;
     35 import android.os.PersistableBundle;
     36 import android.os.RemoteException;
     37 import android.os.SystemClock;
     38 import android.os.UserHandle;
     39 import android.preference.PreferenceManager;
     40 import android.provider.Telephony;
     41 import android.telephony.CarrierConfigManager;
     42 import android.telephony.CellBroadcastMessage;
     43 import android.telephony.SmsCbCmasInfo;
     44 import android.telephony.SmsCbEtwsInfo;
     45 import android.telephony.SmsCbLocation;
     46 import android.telephony.SmsCbMessage;
     47 import android.telephony.SubscriptionManager;
     48 import android.telephony.TelephonyManager;
     49 import android.util.Log;
     50 
     51 import com.android.cellbroadcastreceiver.CellBroadcastChannelManager.CellBroadcastChannelRange;
     52 import com.android.internal.annotations.VisibleForTesting;
     53 import com.android.internal.telephony.PhoneConstants;
     54 
     55 import java.util.ArrayList;
     56 import java.util.LinkedHashMap;
     57 import java.util.Locale;
     58 
     59 /**
     60  * This service manages the display and animation of broadcast messages.
     61  * Emergency messages display with a flashing animated exclamation mark icon,
     62  * and an alert tone is played when the alert is first shown to the user
     63  * (but not when the user views a previously received broadcast).
     64  */
     65 public class CellBroadcastAlertService extends Service {
     66     private static final String TAG = "CBAlertService";
     67 
     68     /** Intent action to display alert dialog/notification, after verifying the alert is new. */
     69     static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
     70 
     71     /** Use the same notification ID for non-emergency alerts. */
     72     static final int NOTIFICATION_ID = 1;
     73 
     74     /**
     75      * Notification channel containing all cellbroadcast broadcast messages notifications.
     76      * Use the same notification channel for non-emergency alerts.
     77      */
     78     static final String NOTIFICATION_CHANNEL_BROADCAST_MESSAGES = "broadcastMessages";
     79 
     80     /** Sticky broadcast for latest area info broadcast received. */
     81     static final String CB_AREA_INFO_RECEIVED_ACTION =
     82             "com.android.cellbroadcastreceiver.CB_AREA_INFO_RECEIVED";
     83 
     84     static final String SETTINGS_APP = "com.android.settings";
     85 
     86     /** Intent extra for passing a SmsCbMessage */
     87     private static final String EXTRA_MESSAGE = "message";
     88 
     89     /**
     90      * Default message expiration time is 24 hours. Same message arrives within 24 hours will be
     91      * treated as a duplicate.
     92      */
     93     private static final long DEFAULT_EXPIRATION_TIME = DAY_IN_MILLIS;
     94 
     95     /**
     96      * Alert type
     97      */
     98     public enum AlertType {
     99         CMAS_DEFAULT,
    100         ETWS_DEFAULT,
    101         EARTHQUAKE,
    102         TSUNAMI,
    103         AREA,
    104         OTHER
    105     }
    106 
    107     /**
    108      *  Container for service category, serial number, location, body hash code, and ETWS primary/
    109      *  secondary information for duplication detection.
    110      */
    111     private static final class MessageServiceCategoryAndScope {
    112         private final int mServiceCategory;
    113         private final int mSerialNumber;
    114         private final SmsCbLocation mLocation;
    115         private final int mBodyHash;
    116         private final boolean mIsEtwsPrimary;
    117 
    118         MessageServiceCategoryAndScope(int serviceCategory, int serialNumber,
    119                 SmsCbLocation location, int bodyHash, boolean isEtwsPrimary) {
    120             mServiceCategory = serviceCategory;
    121             mSerialNumber = serialNumber;
    122             mLocation = location;
    123             mBodyHash = bodyHash;
    124             mIsEtwsPrimary = isEtwsPrimary;
    125         }
    126 
    127         @Override
    128         public int hashCode() {
    129             return mLocation.hashCode() + 5 * mServiceCategory + 7 * mSerialNumber + 13 * mBodyHash
    130                     + 17 * Boolean.hashCode(mIsEtwsPrimary);
    131         }
    132 
    133         @Override
    134         public boolean equals(Object o) {
    135             if (o == this) {
    136                 return true;
    137             }
    138             if (o instanceof MessageServiceCategoryAndScope) {
    139                 MessageServiceCategoryAndScope other = (MessageServiceCategoryAndScope) o;
    140                 return (mServiceCategory == other.mServiceCategory &&
    141                         mSerialNumber == other.mSerialNumber &&
    142                         mLocation.equals(other.mLocation) &&
    143                         mBodyHash == other.mBodyHash &&
    144                         mIsEtwsPrimary == other.mIsEtwsPrimary);
    145             }
    146             return false;
    147         }
    148 
    149         @Override
    150         public String toString() {
    151             return "{mServiceCategory: " + mServiceCategory + " serial number: " + mSerialNumber +
    152                     " location: " + mLocation.toString() + " body hash: " + mBodyHash +
    153                     " mIsEtwsPrimary: " + mIsEtwsPrimary + "}";
    154         }
    155     }
    156 
    157     /** Maximum number of message IDs to save before removing the oldest message ID. */
    158     private static final int MAX_MESSAGE_ID_SIZE = 1024;
    159 
    160     /** Linked hash map of the message identities for duplication detection purposes. The key is the
    161      * the collection of different message keys used for duplication detection, and the value
    162      * is the timestamp of message arriving time. Some carriers may require shorter expiration time.
    163      */
    164     private static final LinkedHashMap<MessageServiceCategoryAndScope, Long> sMessagesMap =
    165             new LinkedHashMap<>();
    166 
    167     @Override
    168     public int onStartCommand(Intent intent, int flags, int startId) {
    169         String action = intent.getAction();
    170         Log.d(TAG, "onStartCommand: " + action);
    171         if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) ||
    172                 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
    173             handleCellBroadcastIntent(intent);
    174         } else if (SHOW_NEW_ALERT_ACTION.equals(action)) {
    175             try {
    176                 if (UserHandle.myUserId() ==
    177                         ActivityManager.getService().getCurrentUser().id) {
    178                     showNewAlert(intent);
    179                 } else {
    180                     Log.d(TAG,"Not active user, ignore the alert display");
    181                 }
    182             } catch (RemoteException e) {
    183                 e.printStackTrace();
    184             }
    185         } else {
    186             Log.e(TAG, "Unrecognized intent action: " + action);
    187         }
    188         return START_NOT_STICKY;
    189     }
    190 
    191     /**
    192      * Get the carrier specific message duplicate expiration time.
    193      *
    194      * @param subId Subscription index
    195      * @return The expiration time in milliseconds. Small values like 0 (or negative values)
    196      * indicate expiration immediately (meaning the duplicate will always be displayed), while large
    197      * values indicate the duplicate will always be ignored. The default value would be 24 hours.
    198      */
    199     private long getDuplicateExpirationTime(int subId) {
    200         CarrierConfigManager configManager = (CarrierConfigManager)
    201                 getApplicationContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
    202         Log.d(TAG, "manager = " + configManager);
    203         if (configManager == null) {
    204             Log.e(TAG, "carrier config is not available.");
    205             return DEFAULT_EXPIRATION_TIME;
    206         }
    207 
    208         PersistableBundle b = configManager.getConfigForSubId(subId);
    209         if (b == null) {
    210             Log.e(TAG, "expiration key does not exist.");
    211             return DEFAULT_EXPIRATION_TIME;
    212         }
    213 
    214         long time = b.getLong(CarrierConfigManager.KEY_MESSAGE_EXPIRATION_TIME_LONG,
    215                 DEFAULT_EXPIRATION_TIME);
    216         return time;
    217     }
    218 
    219     private void handleCellBroadcastIntent(Intent intent) {
    220         Bundle extras = intent.getExtras();
    221         if (extras == null) {
    222             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
    223             return;
    224         }
    225 
    226         SmsCbMessage message = (SmsCbMessage) extras.get(EXTRA_MESSAGE);
    227 
    228         if (message == null) {
    229             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
    230             return;
    231         }
    232 
    233         final CellBroadcastMessage cbm = new CellBroadcastMessage(message);
    234         int subId = intent.getExtras().getInt(PhoneConstants.SUBSCRIPTION_KEY);
    235         if (SubscriptionManager.isValidSubscriptionId(subId)) {
    236             cbm.setSubId(subId);
    237         } else {
    238             Log.e(TAG, "Invalid subscription id");
    239         }
    240 
    241         if (!isMessageEnabledByUser(cbm)) {
    242             Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() +
    243                     " by user preference");
    244             return;
    245         }
    246 
    247         // Check if message body should be used for duplicate detection.
    248         boolean shouldCompareMessageBody =
    249                 getApplicationContext().getResources().getBoolean(R.bool.duplicate_compare_body);
    250 
    251         int hashCode = shouldCompareMessageBody ? message.getMessageBody().hashCode() : 0;
    252 
    253         // If this is an ETWS message, we need to include primary/secondary message information to
    254         // be a factor for duplication detection as well. Per 3GPP TS 23.041 section 8.2,
    255         // duplicate message detection shall be performed independently for primary and secondary
    256         // notifications.
    257         boolean isEtwsPrimary = false;
    258         if (message.isEtwsMessage()) {
    259             SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo();
    260             if (etwsInfo != null) {
    261                 isEtwsPrimary = etwsInfo.isPrimary();
    262             } else {
    263                 Log.w(TAG, "ETWS info is not available.");
    264             }
    265         }
    266 
    267         // Check for duplicate message IDs according to CMAS carrier requirements. Message IDs
    268         // are stored in volatile memory. If the maximum of 1024 messages is reached, the
    269         // message ID of the oldest message is deleted from the list.
    270         MessageServiceCategoryAndScope newCmasId = new MessageServiceCategoryAndScope(
    271                 message.getServiceCategory(), message.getSerialNumber(), message.getLocation(),
    272                 hashCode, isEtwsPrimary);
    273 
    274         Log.d(TAG, "message ID = " + newCmasId);
    275 
    276         long nowTime = SystemClock.elapsedRealtime();
    277         // Check if the identical message arrives again
    278         if (sMessagesMap.get(newCmasId) != null) {
    279             // And if the previous one has not expired yet, treat it as a duplicate message.
    280             long previousTime = sMessagesMap.get(newCmasId);
    281             long expirationTime = getDuplicateExpirationTime(subId);
    282             if (nowTime - previousTime < expirationTime) {
    283                 Log.d(TAG, "ignoring the duplicate alert " + newCmasId + ", nowTime=" + nowTime
    284                         + ", previous=" + previousTime + ", expiration=" + expirationTime);
    285                 return;
    286             }
    287             // otherwise, we don't treat it as a duplicate and will show the same message again.
    288             Log.d(TAG, "The same message shown up " + (nowTime - previousTime)
    289                     + " milliseconds ago. Not a duplicate.");
    290         } else if (sMessagesMap.size() >= MAX_MESSAGE_ID_SIZE){
    291             // If we reach the maximum, remove the first inserted message key.
    292             MessageServiceCategoryAndScope oldestCmasId = sMessagesMap.keySet().iterator().next();
    293             Log.d(TAG, "message ID limit reached, removing oldest message ID " + oldestCmasId);
    294             sMessagesMap.remove(oldestCmasId);
    295         } else {
    296             Log.d(TAG, "New message. Not a duplicate. Map size = " + sMessagesMap.size());
    297         }
    298 
    299         sMessagesMap.put(newCmasId, nowTime);
    300 
    301         final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
    302         alertIntent.setClass(this, CellBroadcastAlertService.class);
    303         alertIntent.putExtra(EXTRA_MESSAGE, cbm);
    304 
    305         // write to database on a background thread
    306         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
    307                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
    308                     @Override
    309                     public boolean execute(CellBroadcastContentProvider provider) {
    310                         if (provider.insertNewBroadcast(cbm)) {
    311                             // new message, show the alert or notification on UI thread
    312                             startService(alertIntent);
    313                             return true;
    314                         } else {
    315                             return false;
    316                         }
    317                     }
    318                 });
    319     }
    320 
    321     private void showNewAlert(Intent intent) {
    322         Bundle extras = intent.getExtras();
    323         if (extras == null) {
    324             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
    325             return;
    326         }
    327 
    328         CellBroadcastMessage cbm = (CellBroadcastMessage) intent.getParcelableExtra(EXTRA_MESSAGE);
    329 
    330         if (cbm == null) {
    331             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
    332             return;
    333         }
    334 
    335         if (isEmergencyMessage(this, cbm)) {
    336             // start alert sound / vibration / TTS and display full-screen alert
    337             openEmergencyAlertNotification(cbm);
    338         } else {
    339             // add notification to the bar by passing the list of unread non-emergency
    340             // CellBroadcastMessages
    341             ArrayList<CellBroadcastMessage> messageList = CellBroadcastReceiverApp
    342                     .addNewMessageToList(cbm);
    343             addToNotificationBar(cbm, messageList, this, false);
    344         }
    345     }
    346 
    347     /**
    348      * Check if the device is currently on roaming.
    349      *
    350      * @param subId Subscription index
    351      * @return True if roaming, otherwise not roaming.
    352      */
    353     private boolean isRoaming(int subId) {
    354         Context context = getApplicationContext();
    355 
    356         if (context != null) {
    357             TelephonyManager tm =
    358                     (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    359             return tm.isNetworkRoaming(subId);
    360         }
    361 
    362         return false;
    363     }
    364 
    365     /**
    366      * Filter out broadcasts on the test channels that the user has not enabled,
    367      * and types of notifications that the user is not interested in receiving.
    368      * This allows us to enable an entire range of message identifiers in the
    369      * radio and not have to explicitly disable the message identifiers for
    370      * test broadcasts. In the unlikely event that the default shared preference
    371      * values were not initialized in CellBroadcastReceiverApp, the second parameter
    372      * to the getBoolean() calls match the default values in res/xml/preferences.xml.
    373      *
    374      * @param message the message to check
    375      * @return true if the user has enabled this message type; false otherwise
    376      */
    377     private boolean isMessageEnabledByUser(CellBroadcastMessage message) {
    378 
    379         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    380         // Check if all emergency alerts are disabled.
    381         boolean emergencyAlertEnabled =
    382                 prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS, true);
    383 
    384         // Check if ETWS/CMAS test message is forced to disabled on the device.
    385         boolean forceDisableEtwsCmasTest =
    386                 CellBroadcastSettings.isFeatureEnabled(this,
    387                         CarrierConfigManager.KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false);
    388 
    389         boolean enableAreaUpdateInfoAlerts = Resources.getSystem().getBoolean(
    390                 com.android.internal.R.bool.config_showAreaUpdateInfoSettings)
    391                 && prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_AREA_UPDATE_INFO_ALERTS,
    392                 false);
    393 
    394         if (message.isEtwsTestMessage()) {
    395             return emergencyAlertEnabled &&
    396                     !forceDisableEtwsCmasTest &&
    397                     PreferenceManager.getDefaultSharedPreferences(this)
    398                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false);
    399         }
    400 
    401         if (message.isEtwsMessage()) {
    402             // ETWS messages.
    403             // Turn on/off emergency notifications is the only way to turn on/off ETWS messages.
    404             return emergencyAlertEnabled;
    405 
    406         }
    407 
    408         int channel = message.getServiceCategory();
    409 
    410         // Check if the messages are on additional channels enabled by the resource config.
    411         // If those channels are enabled by the carrier, but the device is actually roaming, we
    412         // should not allow the messages.
    413         ArrayList<CellBroadcastChannelRange> ranges = CellBroadcastChannelManager
    414                 .getInstance().getCellBroadcastChannelRanges(getApplicationContext());
    415 
    416         if (ranges != null) {
    417             for (CellBroadcastChannelRange range : ranges) {
    418                 if (range.mStartId <= channel && range.mEndId >= channel) {
    419                     // We only enable the channels when the device is not roaming.
    420                     if (isRoaming(message.getSubId())) {
    421                         return false;
    422                     }
    423 
    424                     // The area update information cell broadcast should not cause any pop-up.
    425                     // Instead the setting's app SIM status will show its information.
    426                     if (range.mAlertType == AlertType.AREA) {
    427                         if (enableAreaUpdateInfoAlerts) {
    428                             // save latest area info broadcast for Settings display and send as
    429                             // broadcast.
    430                             CellBroadcastReceiverApp.setLatestAreaInfo(message);
    431                             Intent intent = new Intent(CB_AREA_INFO_RECEIVED_ACTION);
    432                             intent.setPackage(SETTINGS_APP);
    433                             intent.putExtra(EXTRA_MESSAGE, message);
    434                             // Send broadcast twice, once for apps that have PRIVILEGED permission
    435                             // and once for those that have the runtime one.
    436                             sendBroadcastAsUser(intent, UserHandle.ALL,
    437                                     android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
    438                             sendBroadcastAsUser(intent, UserHandle.ALL,
    439                                     android.Manifest.permission.READ_PHONE_STATE);
    440                             // area info broadcasts are displayed in Settings status screen
    441                         }
    442                         return false;
    443                     }
    444 
    445                     return emergencyAlertEnabled;
    446                 }
    447             }
    448         }
    449 
    450         if (message.isCmasMessage()) {
    451             switch (message.getCmasMessageClass()) {
    452                 case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
    453                     return emergencyAlertEnabled &&
    454                             PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    455                             CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
    456 
    457                 case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
    458                     return emergencyAlertEnabled &&
    459                             PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    460                             CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
    461 
    462                 case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
    463                     return emergencyAlertEnabled &&
    464                             PreferenceManager.getDefaultSharedPreferences(this)
    465                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
    466 
    467                 case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
    468                 case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
    469                 case SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE:
    470                     return emergencyAlertEnabled &&
    471                             !forceDisableEtwsCmasTest &&
    472                             PreferenceManager.getDefaultSharedPreferences(this)
    473                                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS,
    474                                             false);
    475                 default:
    476                     return true;    // presidential-level CMAS alerts are always enabled
    477             }
    478         }
    479 
    480         return true;    // other broadcast messages are always enabled
    481     }
    482 
    483     /**
    484      * Display an alert message for emergency alerts.
    485      * @param message the alert to display
    486      */
    487     private void openEmergencyAlertNotification(CellBroadcastMessage message) {
    488         // Acquire a screen bright wakelock until the alert dialog and audio start playing.
    489         CellBroadcastAlertWakeLock.acquireScreenBrightWakeLock(this);
    490 
    491         // Close dialogs and window shade
    492         Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
    493         sendBroadcast(closeDialogs);
    494 
    495         // start audio/vibration/speech service for emergency alerts
    496         Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
    497         audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
    498         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    499 
    500         AlertType alertType = AlertType.CMAS_DEFAULT;
    501         if (message.isEtwsMessage()) {
    502             // For ETWS, always vibrate, even in silent mode.
    503             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA, true);
    504             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_ETWS_VIBRATE_EXTRA, true);
    505             alertType = AlertType.ETWS_DEFAULT;
    506 
    507             if (message.getEtwsWarningInfo() != null) {
    508                 int warningType = message.getEtwsWarningInfo().getWarningType();
    509 
    510                 switch (warningType) {
    511                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE:
    512                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
    513                         alertType = AlertType.EARTHQUAKE;
    514                         break;
    515                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI:
    516                         alertType = AlertType.TSUNAMI;
    517                         break;
    518                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY:
    519                         alertType = AlertType.OTHER;
    520                         break;
    521                 }
    522             }
    523         } else {
    524             // For other alerts, vibration can be disabled in app settings.
    525             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA,
    526                     prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true));
    527             int channel = message.getServiceCategory();
    528             ArrayList<CellBroadcastChannelRange> ranges = CellBroadcastChannelManager
    529                     .getInstance().getCellBroadcastChannelRanges(getApplicationContext());
    530             if (ranges != null) {
    531                 for (CellBroadcastChannelRange range : ranges) {
    532                     if (channel >= range.mStartId && channel <= range.mEndId) {
    533                         alertType = range.mAlertType;
    534                         break;
    535                     }
    536                 }
    537             }
    538         }
    539         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE, alertType);
    540 
    541         String messageBody = message.getMessageBody();
    542 
    543         if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) {
    544             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
    545 
    546             String preferredLanguage = message.getLanguageCode();
    547             String defaultLanguage = null;
    548             if (message.isEtwsMessage()) {
    549                 // Only do TTS for ETWS secondary message.
    550                 // There is no text in ETWS primary message. When we construct the ETWS primary
    551                 // message, we hardcode "ETWS" as the body hence we don't want to speak that out
    552                 // here.
    553 
    554                 // Also in many cases we see the secondary message comes few milliseconds after
    555                 // the primary one. If we play TTS for the primary one, It will be overwritten by
    556                 // the secondary one immediately anyway.
    557                 if (!message.getEtwsWarningInfo().isPrimary()) {
    558                     // Since only Japanese carriers are using ETWS, if there is no language
    559                     // specified in the ETWS message, we'll use Japanese as the default language.
    560                     defaultLanguage = "ja";
    561                 }
    562             } else {
    563                 // If there is no language specified in the CMAS message, use device's
    564                 // default language.
    565                 defaultLanguage = Locale.getDefault().getLanguage();
    566             }
    567 
    568             Log.d(TAG, "Preferred language = " + preferredLanguage +
    569                     ", Default language = " + defaultLanguage);
    570             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE,
    571                     preferredLanguage);
    572             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE,
    573                     defaultLanguage);
    574         }
    575         startService(audioIntent);
    576 
    577         ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1);
    578         messageList.add(message);
    579 
    580         // For FEATURE_WATCH, the dialog doesn't make sense from a UI/UX perspective
    581         if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
    582             addToNotificationBar(message, messageList, this, false);
    583         } else {
    584             Intent alertDialogIntent = createDisplayMessageIntent(this,
    585                     CellBroadcastAlertDialog.class, messageList);
    586             alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    587             startActivity(alertDialogIntent);
    588         }
    589 
    590     }
    591 
    592     /**
    593      * Add the new alert to the notification bar (non-emergency alerts), or launch a
    594      * high-priority immediate intent for emergency alerts.
    595      * @param message the alert to display
    596      */
    597     static void addToNotificationBar(CellBroadcastMessage message,
    598                                      ArrayList<CellBroadcastMessage> messageList, Context context,
    599                                      boolean fromSaveState) {
    600         int channelTitleId = CellBroadcastResources.getDialogTitleResource(context, message);
    601         CharSequence channelName = context.getText(channelTitleId);
    602         String messageBody = message.getMessageBody();
    603         final NotificationManager notificationManager = NotificationManager.from(context);
    604         createNotificationChannels(context);
    605 
    606         // Create intent to show the new messages when user selects the notification.
    607         Intent intent;
    608         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
    609             // For FEATURE_WATCH we want to mark as read
    610             intent = createMarkAsReadIntent(context, message.getDeliveryTime());
    611         } else {
    612             // For anything else we handle it normally
    613             intent = createDisplayMessageIntent(context, CellBroadcastAlertDialog.class,
    614                     messageList);
    615         }
    616 
    617         intent.putExtra(CellBroadcastAlertDialog.FROM_NOTIFICATION_EXTRA, true);
    618         intent.putExtra(CellBroadcastAlertDialog.FROM_SAVE_STATE_NOTIFICATION_EXTRA, fromSaveState);
    619 
    620         PendingIntent pi;
    621         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
    622             pi = PendingIntent.getBroadcast(context, 0, intent, 0);
    623         } else {
    624             pi = PendingIntent.getActivity(context, NOTIFICATION_ID, intent,
    625                     PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    626         }
    627 
    628         // use default sound/vibration/lights for non-emergency broadcasts
    629         Notification.Builder builder = new Notification.Builder(
    630                 context, NOTIFICATION_CHANNEL_BROADCAST_MESSAGES)
    631                 .setSmallIcon(R.drawable.ic_notify_alert)
    632                 .setTicker(channelName)
    633                 .setWhen(System.currentTimeMillis())
    634                 .setCategory(Notification.CATEGORY_SYSTEM)
    635                 .setPriority(Notification.PRIORITY_HIGH)
    636                 .setColor(context.getResources().getColor(R.color.notification_color))
    637                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    638                 .setDefaults(Notification.DEFAULT_ALL);
    639 
    640         builder.setDefaults(Notification.DEFAULT_ALL);
    641 
    642         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
    643             builder.setDeleteIntent(pi);
    644         } else {
    645             builder.setContentIntent(pi);
    646         }
    647 
    648         // increment unread alert count (decremented when user dismisses alert dialog)
    649         int unreadCount = messageList.size();
    650         if (unreadCount > 1) {
    651             // use generic count of unread broadcasts if more than one unread
    652             builder.setContentTitle(context.getString(R.string.notification_multiple_title));
    653             builder.setContentText(context.getString(R.string.notification_multiple, unreadCount));
    654         } else {
    655             builder.setContentTitle(channelName).setContentText(messageBody);
    656         }
    657 
    658         notificationManager.notify(NOTIFICATION_ID, builder.build());
    659     }
    660 
    661     /**
    662      * Creates the notification channel and registers it with NotificationManager. If a channel
    663      * with the same ID is already registered, NotificationManager will ignore this call.
    664      */
    665     static void createNotificationChannels(Context context) {
    666         NotificationManager.from(context).createNotificationChannel(
    667                 new NotificationChannel(
    668                 NOTIFICATION_CHANNEL_BROADCAST_MESSAGES,
    669                 context.getString(R.string.notification_channel_broadcast_messages),
    670                 NotificationManager.IMPORTANCE_LOW));
    671     }
    672 
    673     static Intent createDisplayMessageIntent(Context context, Class intentClass,
    674             ArrayList<CellBroadcastMessage> messageList) {
    675         // Trigger the list activity to fire up a dialog that shows the received messages
    676         Intent intent = new Intent(context, intentClass);
    677         intent.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, messageList);
    678         return intent;
    679     }
    680 
    681     /**
    682      * Creates a delete intent that calls to the {@link CellBroadcastReceiver} in order to mark
    683      * a message as read
    684      *
    685      * @param context context of the caller
    686      * @param deliveryTime time the message was sent in order to mark as read
    687      * @return delete intent to add to the pending intent
    688      */
    689     static Intent createMarkAsReadIntent(Context context, long deliveryTime) {
    690         Intent deleteIntent = new Intent(context, CellBroadcastReceiver.class);
    691         deleteIntent.setAction(CellBroadcastReceiver.ACTION_MARK_AS_READ);
    692         deleteIntent.putExtra(CellBroadcastReceiver.EXTRA_DELIVERY_TIME, deliveryTime);
    693         return deleteIntent;
    694     }
    695 
    696     @VisibleForTesting
    697     @Override
    698     public IBinder onBind(Intent intent) {
    699         return new LocalBinder();
    700     }
    701 
    702     @VisibleForTesting
    703     class LocalBinder extends Binder {
    704         public CellBroadcastAlertService getService() {
    705             return CellBroadcastAlertService.this;
    706         }
    707     }
    708 
    709     /**
    710      * Check if the cell broadcast message is an emergency message or not
    711      * @param context Device context
    712      * @param cbm Cell broadcast message
    713      * @return True if the message is an emergency message, otherwise false.
    714      */
    715     public static boolean isEmergencyMessage(Context context, CellBroadcastMessage cbm) {
    716         boolean isEmergency = false;
    717 
    718         if (cbm == null) {
    719             return false;
    720         }
    721 
    722         int id = cbm.getServiceCategory();
    723 
    724         if (cbm.isEmergencyAlertMessage()) {
    725             isEmergency = true;
    726         } else {
    727             ArrayList<CellBroadcastChannelRange> ranges = CellBroadcastChannelManager
    728                     .getInstance().getCellBroadcastChannelRanges(context);
    729 
    730             if (ranges != null) {
    731                 for (CellBroadcastChannelRange range : ranges) {
    732                     if (range.mStartId <= id && range.mEndId >= id) {
    733                         isEmergency = range.mIsEmergency;
    734                         break;
    735                     }
    736                 }
    737             }
    738         }
    739 
    740         Log.d(TAG, "isEmergencyMessage: " + isEmergency + "message id = " + id);
    741         return isEmergency;
    742     }
    743 }
    744