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