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 MessageServiceCategoryAndScope(int serviceCategory, int serialNumber, SmsCbLocation location, int bodyHash, boolean isEtwsPrimary)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 hashCode()113 public int hashCode() { 114 return mLocation.hashCode() + 5 * mServiceCategory + 7 * mSerialNumber + 13 * mBodyHash 115 + 17 * Boolean.hashCode(mIsEtwsPrimary); 116 } 117 118 @Override equals(Object o)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 toString()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 onStartCommand(Intent intent, int flags, int startId)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 */ getDuplicateExpirationTime(int subId)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 handleCellBroadcastIntent(Intent intent)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 showNewAlert(Intent intent)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 */ isMessageEnabledByUser(CellBroadcastMessage message)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 */ openEmergencyAlertNotification(CellBroadcastMessage message)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 */ addToNotificationBar(CellBroadcastMessage message, ArrayList<CellBroadcastMessage> messageList, Context context, boolean fromSaveState)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 */ createNotificationChannels(Context context)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 createDisplayMessageIntent(Context context, Class intentClass, ArrayList<CellBroadcastMessage> messageList)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 */ createMarkAsReadIntent(Context context, long deliveryTime)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 onBind(Intent intent)633 public IBinder onBind(Intent intent) { 634 return new LocalBinder(); 635 } 636 637 @VisibleForTesting 638 class LocalBinder extends Binder { getService()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 */ isEmergencyMessage(Context context, CellBroadcastMessage cbm)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