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