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