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