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.annotation.NonNull; 20 import android.app.ActivityManager; 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.app.Service; 26 import android.content.ContentValues; 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.media.AudioAttributes; 33 import android.media.AudioManager; 34 import android.net.Uri; 35 import android.os.Binder; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.IBinder; 39 import android.os.Looper; 40 import android.os.SystemProperties; 41 import android.os.UserHandle; 42 import android.preference.PreferenceManager; 43 import android.provider.Telephony; 44 import android.service.notification.StatusBarNotification; 45 import android.telephony.PhoneStateListener; 46 import android.telephony.SmsCbEtwsInfo; 47 import android.telephony.SmsCbMessage; 48 import android.telephony.SubscriptionManager; 49 import android.telephony.TelephonyManager; 50 import android.text.TextUtils; 51 import android.util.Log; 52 53 import com.android.cellbroadcastreceiver.CellBroadcastChannelManager.CellBroadcastChannelRange; 54 import com.android.internal.annotations.VisibleForTesting; 55 56 import java.util.ArrayList; 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 implements AudioManager.OnAudioFocusChangeListener { 67 private static final String TAG = "CBAlertService"; 68 69 /** Intent action to display alert dialog/notification, after verifying the alert is new. */ 70 @VisibleForTesting 71 public static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT"; 72 73 /** Identifier for getExtra() when adding this object to an Intent. */ 74 public static final String SMS_CB_MESSAGE_EXTRA = 75 "com.android.cellbroadcastreceiver.SMS_CB_MESSAGE"; 76 77 /** Use the same notification ID for non-emergency alerts. */ 78 @VisibleForTesting 79 public static final int NOTIFICATION_ID = 1; 80 81 /** 82 * Notification channel containing for non-emergency alerts. 83 */ 84 static final String NOTIFICATION_CHANNEL_NON_EMERGENCY_ALERTS = "broadcastMessagesNonEmergency"; 85 86 /** 87 * Notification channel for emergency alerts. This is used when users sneak out of the 88 * noisy pop-up for a real emergency and get a notification due to not officially acknowledged 89 * the alert and want to refer it back later. 90 */ 91 static final String NOTIFICATION_CHANNEL_EMERGENCY_ALERTS = "broadcastMessages"; 92 93 /** 94 * Notification channel for emergency alerts during voice call. This is used when users in a 95 * voice call, emergency alert will be displayed in a notification format rather than playing 96 * alert tone. 97 */ 98 static final String NOTIFICATION_CHANNEL_EMERGENCY_ALERTS_IN_VOICECALL = 99 "broadcastMessagesInVoiceCall"; 100 101 /** Intent extra for passing a SmsCbMessage */ 102 private static final String EXTRA_MESSAGE = "message"; 103 104 /** 105 * Key for accessing message filter from SystemProperties. For testing use. 106 */ 107 private static final String MESSAGE_FILTER_PROPERTY_KEY = 108 "persist.cellbroadcast.message_filter"; 109 110 private Context mContext; 111 112 /** 113 * Alert type 114 */ 115 public enum AlertType { 116 DEFAULT, 117 ETWS_DEFAULT, 118 ETWS_EARTHQUAKE, 119 ETWS_TSUNAMI, 120 TEST, 121 AREA, 122 INFO, 123 OTHER 124 } 125 126 private TelephonyManager mTelephonyManager; 127 private AudioManager mAudioManager; 128 129 /** 130 * Do not preempt active voice call, instead post notifications and play the ringtone/vibrate 131 * when the voicecall finish 132 */ 133 private static boolean sRemindAfterCallFinish = false; 134 135 136 @Override onStartCommand(Intent intent, int flags, int startId)137 public int onStartCommand(Intent intent, int flags, int startId) { 138 mContext = getApplicationContext(); 139 String action = intent.getAction(); 140 Log.d(TAG, "onStartCommand: " + action); 141 if (Telephony.Sms.Intents.ACTION_SMS_EMERGENCY_CB_RECEIVED.equals(action) || 142 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) { 143 handleCellBroadcastIntent(intent); 144 } else if (SHOW_NEW_ALERT_ACTION.equals(action)) { 145 if (UserHandle.myUserId() == ((ActivityManager) getSystemService( 146 Context.ACTIVITY_SERVICE)).getCurrentUser()) { 147 showNewAlert(intent); 148 } else { 149 Log.d(TAG, "Not active user, ignore the alert display"); 150 } 151 } else { 152 Log.e(TAG, "Unrecognized intent action: " + action); 153 } 154 return START_NOT_STICKY; 155 } 156 157 @Override onCreate()158 public void onCreate() { 159 mTelephonyManager = (TelephonyManager) 160 getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE); 161 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 162 mAudioManager = (AudioManager) 163 getApplicationContext().getSystemService(Context.AUDIO_SERVICE); 164 } 165 166 @Override onDestroy()167 public void onDestroy() { 168 // Stop listening for incoming calls. 169 mTelephonyManager.listen(mPhoneStateListener, 0); 170 171 } 172 173 /** 174 * Check if we should display the received cell broadcast message. 175 * 176 * @param message Cell broadcast message 177 * @return True if the message should be displayed to the user 178 */ shouldDisplayMessage(SmsCbMessage message)179 private boolean shouldDisplayMessage(SmsCbMessage message) { 180 TelephonyManager tm = ((TelephonyManager) mContext.getSystemService( 181 Context.TELEPHONY_SERVICE)).createForSubscriptionId(message.getSubscriptionId()); 182 if (tm.getEmergencyCallbackMode() && CellBroadcastSettings.getResources( 183 mContext, message.getSubscriptionId()).getBoolean(R.bool.ignore_messages_in_ecbm)) { 184 // Ignore the message in ECBM. 185 // It is for LTE only mode. For 1xRTT, incoming pages should be ignored in the modem. 186 Log.d(TAG, "ignoring alert of type " + message.getServiceCategory() + " in ECBM"); 187 return false; 188 } 189 // Check if the channel is enabled by the user or configuration. 190 if (!isChannelEnabled(message)) { 191 Log.d(TAG, "ignoring alert of type " + message.getServiceCategory() 192 + " by user preference"); 193 return false; 194 } 195 196 // Check if message body is empty 197 String msgBody = message.getMessageBody(); 198 if (msgBody == null || msgBody.length() == 0) { 199 Log.e(TAG, "Empty content or Unsupported charset"); 200 return false; 201 } 202 203 // Check if we need to perform language filtering. 204 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(mContext, 205 message.getSubscriptionId()); 206 CellBroadcastChannelRange range = channelManager 207 .getCellBroadcastChannelRangeFromMessage(message); 208 String messageLanguage = message.getLanguageCode(); 209 if (range != null && range.mFilterLanguage) { 210 // language filtering based on CBR second language settings 211 final String secondLanguageCode = CellBroadcastSettings.getResources(mContext, 212 SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) 213 .getString(R.string.emergency_alert_second_language_code); 214 if (!secondLanguageCode.isEmpty()) { 215 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 216 boolean receiveInSecondLanguage = prefs.getBoolean( 217 CellBroadcastSettings.KEY_RECEIVE_CMAS_IN_SECOND_LANGUAGE, false); 218 // For DCS values that bit 6 is 1 and bit 7 is 0, language field is not defined so 219 // ap receives it as null value and so alert is not shown to the user. 220 // bypass language filter in this case. 221 if (!TextUtils.isEmpty(messageLanguage) 222 && !secondLanguageCode.equalsIgnoreCase(messageLanguage)) { 223 Log.w(TAG, "Ignoring message in the unspecified second language:" 224 + messageLanguage); 225 return false; 226 } else if (!receiveInSecondLanguage) { 227 Log.d(TAG, "Ignoring message in second language because setting is off"); 228 return false; 229 } 230 } else { 231 // language filtering based on device language settings. 232 String deviceLanguage = Locale.getDefault().getLanguage(); 233 // Apply If the message's language does not match device's message, we don't 234 // display the message. 235 if (!TextUtils.isEmpty(messageLanguage) 236 && !messageLanguage.equalsIgnoreCase(deviceLanguage)) { 237 Log.d(TAG, "ignoring the alert due to language mismatch. Message lang=" 238 + messageLanguage + ", device lang=" + deviceLanguage); 239 return false; 240 } 241 } 242 } 243 244 // Check for custom filtering 245 String messageFilters = SystemProperties.get(MESSAGE_FILTER_PROPERTY_KEY, ""); 246 if (!TextUtils.isEmpty(messageFilters)) { 247 String[] filters = messageFilters.split(","); 248 for (String filter : filters) { 249 if (!TextUtils.isEmpty(filter)) { 250 if (message.getMessageBody().toLowerCase().contains(filter)) { 251 Log.i(TAG, "Skipped message due to filter: " + filter); 252 return false; 253 } 254 } 255 } 256 } 257 258 return true; 259 } 260 handleCellBroadcastIntent(Intent intent)261 private void handleCellBroadcastIntent(Intent intent) { 262 Bundle extras = intent.getExtras(); 263 if (extras == null) { 264 Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!"); 265 return; 266 } 267 268 SmsCbMessage message = (SmsCbMessage) extras.get(EXTRA_MESSAGE); 269 270 if (message == null) { 271 Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra"); 272 return; 273 } 274 275 if (!shouldDisplayMessage(message)) { 276 return; 277 } 278 279 final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION); 280 alertIntent.setClass(this, CellBroadcastAlertService.class); 281 alertIntent.putExtra(EXTRA_MESSAGE, message); 282 283 // write to database on a background thread 284 new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver()) 285 .execute((CellBroadcastContentProvider.CellBroadcastOperation) provider -> { 286 if (provider.insertNewBroadcast(message)) { 287 // new message, show the alert or notification on UI thread 288 startService(alertIntent); 289 // mark the message as displayed to the user. 290 markMessageDisplayed(message); 291 if (CellBroadcastSettings.getResources(mContext, 292 message.getSubscriptionId()) 293 .getBoolean(R.bool.enable_write_alerts_to_sms_inbox)) { 294 // TODO: Should not create the instance of channel manager everywhere. 295 CellBroadcastChannelManager channelManager = 296 new CellBroadcastChannelManager(mContext, 297 message.getSubscriptionId()); 298 CellBroadcastChannelRange range = channelManager 299 .getCellBroadcastChannelRangeFromMessage(message); 300 if (CellBroadcastReceiver.isTestingMode(getApplicationContext()) 301 || range.mWriteToSmsInbox) { 302 writeMessageToSmsInbox(message); 303 } 304 } 305 return true; 306 } else { 307 return false; 308 } 309 }); 310 } 311 312 /** 313 * Mark the message as displayed in cell broadcast service's database. 314 * 315 * @param message The cell broadcast message. 316 */ markMessageDisplayed(SmsCbMessage message)317 private void markMessageDisplayed(SmsCbMessage message) { 318 ContentValues cv = new ContentValues(); 319 cv.put(Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 1); 320 mContext.getContentResolver().update(Telephony.CellBroadcasts.CONTENT_URI, cv, 321 Telephony.CellBroadcasts.RECEIVED_TIME + "=?", 322 new String[] {Long.toString(message.getReceivedTime())}); 323 } 324 showNewAlert(Intent intent)325 private void showNewAlert(Intent intent) { 326 Bundle extras = intent.getExtras(); 327 if (extras == null) { 328 Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!"); 329 return; 330 } 331 332 SmsCbMessage cbm = intent.getParcelableExtra(EXTRA_MESSAGE); 333 334 if (cbm == null) { 335 Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra"); 336 return; 337 } 338 339 if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE 340 && CellBroadcastSettings.getResources(mContext, cbm.getSubscriptionId()) 341 .getBoolean(R.bool.enable_alert_handling_during_call)) { 342 Log.d(TAG, "CMAS received in dialing/during voicecall."); 343 sRemindAfterCallFinish = true; 344 } 345 346 // Either shown the dialog, adding it to notification (non emergency, or delayed emergency), 347 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 348 mContext, cbm.getSubscriptionId()); 349 if (channelManager.isEmergencyMessage(cbm) && !sRemindAfterCallFinish) { 350 // start alert sound / vibration / TTS and display full-screen alert 351 openEmergencyAlertNotification(cbm); 352 } else { 353 // add notification to the bar by passing the list of unread non-emergency 354 // cell broadcast messages 355 ArrayList<SmsCbMessage> messageList = CellBroadcastReceiverApp 356 .addNewMessageToList(cbm); 357 addToNotificationBar(cbm, messageList, this, false); 358 } 359 } 360 361 /** 362 * Check if the message's channel is enabled on the device. 363 * 364 * @param message the message to check 365 * @return true if the channel is enabled on the device, otherwise false. 366 */ isChannelEnabled(SmsCbMessage message)367 private boolean isChannelEnabled(SmsCbMessage message) { 368 369 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 370 // Check if all emergency alerts are disabled. 371 boolean emergencyAlertEnabled = 372 prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); 373 374 SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo(); 375 if (etwsInfo != null 376 && etwsInfo.getWarningType() == SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE) { 377 return emergencyAlertEnabled 378 && CellBroadcastSettings.isTestAlertsToggleVisible(getApplicationContext()) 379 && PreferenceManager.getDefaultSharedPreferences(this) 380 .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, false); 381 } 382 383 if (message.isEtwsMessage()) { 384 // ETWS messages. 385 // Turn on/off emergency notifications is the only way to turn on/off ETWS messages. 386 return emergencyAlertEnabled; 387 388 } 389 390 int channel = message.getServiceCategory(); 391 392 // Check if the messages are on additional channels enabled by the resource config. 393 // If those channels are enabled by the carrier, but the device is actually roaming, we 394 // should not allow the messages. 395 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 396 mContext, message.getSubscriptionId()); 397 ArrayList<CellBroadcastChannelRange> ranges = channelManager.getCellBroadcastChannelRanges( 398 R.array.additional_cbs_channels_strings); 399 400 for (CellBroadcastChannelRange range : ranges) { 401 if (range.mStartId <= channel && range.mEndId >= channel) { 402 // Check if the channel is within the scope. If not, ignore the alert message. 403 if (!channelManager.checkScope(range.mScope)) { 404 Log.d(TAG, "The range [" + range.mStartId + "-" + range.mEndId 405 + "] is not within the scope. mScope = " + range.mScope); 406 return false; 407 } 408 409 if (range.mAlertType == AlertType.TEST) { 410 return emergencyAlertEnabled 411 && CellBroadcastSettings.isTestAlertsToggleVisible( 412 getApplicationContext()) 413 && PreferenceManager.getDefaultSharedPreferences(this) 414 .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, 415 false); 416 } 417 418 return emergencyAlertEnabled; 419 } 420 } 421 422 if (channelManager.checkCellBroadcastChannelRange(channel, 423 R.array.emergency_alerts_channels_range_strings)) { 424 return emergencyAlertEnabled 425 && PreferenceManager.getDefaultSharedPreferences(this).getBoolean( 426 CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS, true); 427 } 428 // CMAS warning types 429 if (channelManager.checkCellBroadcastChannelRange(channel, 430 R.array.cmas_presidential_alerts_channels_range_strings)) { 431 // always enabled 432 return true; 433 } 434 if (channelManager.checkCellBroadcastChannelRange(channel, 435 R.array.cmas_alert_extreme_channels_range_strings)) { 436 return emergencyAlertEnabled 437 && PreferenceManager.getDefaultSharedPreferences(this).getBoolean( 438 CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true); 439 } 440 if (channelManager.checkCellBroadcastChannelRange(channel, 441 R.array.cmas_alerts_severe_range_strings)) { 442 return emergencyAlertEnabled 443 && PreferenceManager.getDefaultSharedPreferences(this).getBoolean( 444 CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true); 445 } 446 if (channelManager.checkCellBroadcastChannelRange(channel, 447 R.array.cmas_amber_alerts_channels_range_strings)) { 448 return emergencyAlertEnabled 449 && PreferenceManager.getDefaultSharedPreferences(this) 450 .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true); 451 } 452 453 if (channelManager.checkCellBroadcastChannelRange(channel, 454 R.array.exercise_alert_range_strings) 455 && getResources().getBoolean(R.bool.always_enable_exercise_alert)) { 456 return true; 457 } 458 459 if (channelManager.checkCellBroadcastChannelRange(channel, 460 R.array.required_monthly_test_range_strings) 461 || channelManager.checkCellBroadcastChannelRange(channel, 462 R.array.exercise_alert_range_strings) 463 || channelManager.checkCellBroadcastChannelRange(channel, 464 R.array.operator_defined_alert_range_strings)) { 465 return emergencyAlertEnabled 466 && CellBroadcastSettings.isTestAlertsToggleVisible(getApplicationContext()) 467 && PreferenceManager.getDefaultSharedPreferences(this) 468 .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, 469 false); 470 } 471 472 if (channelManager.checkCellBroadcastChannelRange(channel, 473 R.array.public_safety_messages_channels_range_strings)) { 474 return emergencyAlertEnabled 475 && PreferenceManager.getDefaultSharedPreferences(this) 476 .getBoolean(CellBroadcastSettings.KEY_ENABLE_PUBLIC_SAFETY_MESSAGES, 477 true); 478 } 479 480 if (channelManager.checkCellBroadcastChannelRange(channel, 481 R.array.state_local_test_alert_range_strings)) { 482 return emergencyAlertEnabled 483 && PreferenceManager.getDefaultSharedPreferences(this) 484 .getBoolean(CellBroadcastSettings.KEY_ENABLE_STATE_LOCAL_TEST_ALERTS, 485 false); 486 } 487 488 return true; 489 } 490 491 /** 492 * Display an alert message for emergency alerts. 493 * @param message the alert to display 494 */ openEmergencyAlertNotification(SmsCbMessage message)495 private void openEmergencyAlertNotification(SmsCbMessage message) { 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 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 506 mContext, message.getSubscriptionId()); 507 508 AlertType alertType = AlertType.DEFAULT; 509 if (message.isEtwsMessage()) { 510 alertType = AlertType.ETWS_DEFAULT; 511 512 if (message.getEtwsWarningInfo() != null) { 513 int warningType = message.getEtwsWarningInfo().getWarningType(); 514 515 switch (warningType) { 516 case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE: 517 case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI: 518 alertType = AlertType.ETWS_EARTHQUAKE; 519 break; 520 case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI: 521 alertType = AlertType.ETWS_TSUNAMI; 522 break; 523 case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE: 524 alertType = AlertType.TEST; 525 break; 526 case SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY: 527 alertType = AlertType.OTHER; 528 break; 529 } 530 } 531 } else { 532 int channel = message.getServiceCategory(); 533 ArrayList<CellBroadcastChannelRange> ranges = channelManager 534 .getAllCellBroadcastChannelRanges(); 535 for (CellBroadcastChannelRange range : ranges) { 536 if (channel >= range.mStartId && channel <= range.mEndId) { 537 alertType = range.mAlertType; 538 break; 539 } 540 } 541 } 542 CellBroadcastChannelRange range = channelManager 543 .getCellBroadcastChannelRangeFromMessage(message); 544 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE, alertType); 545 audioIntent.putExtra( 546 CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATION_PATTERN_EXTRA, 547 (range != null) 548 ? range.mVibrationPattern 549 : CellBroadcastSettings.getResources(mContext, message.getSubscriptionId()) 550 .getIntArray(R.array.default_vibration_pattern)); 551 552 if (prefs.getBoolean(CellBroadcastSettings.KEY_OVERRIDE_DND, false) 553 || (range != null && range.mOverrideDnd)) { 554 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_OVERRIDE_DND_EXTRA, true); 555 } 556 557 String messageBody = message.getMessageBody(); 558 559 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody); 560 561 String language = message.getLanguageCode(); 562 563 Log.d(TAG, "Message language = " + language); 564 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE, 565 language); 566 567 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_SUB_INDEX, 568 message.getSubscriptionId()); 569 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION, 570 (range != null) ? range.mAlertDuration : -1); 571 startService(audioIntent); 572 573 ArrayList<SmsCbMessage> messageList = new ArrayList<>(); 574 messageList.add(message); 575 576 // For FEATURE_WATCH, the dialog doesn't make sense from a UI/UX perspective 577 if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { 578 addToNotificationBar(message, messageList, this, false); 579 } else { 580 Intent alertDialogIntent = createDisplayMessageIntent(this, 581 CellBroadcastAlertDialog.class, messageList); 582 alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 583 startActivity(alertDialogIntent); 584 } 585 586 } 587 588 /** 589 * Add the new alert to the notification bar (non-emergency alerts), or launch a 590 * high-priority immediate intent for emergency alerts. 591 * @param message the alert to display 592 */ addToNotificationBar(SmsCbMessage message, ArrayList<SmsCbMessage> messageList, Context context, boolean fromSaveState)593 static void addToNotificationBar(SmsCbMessage message, 594 ArrayList<SmsCbMessage> messageList, Context context, 595 boolean fromSaveState) { 596 Resources res = CellBroadcastSettings.getResources(context, message.getSubscriptionId()); 597 int channelTitleId = CellBroadcastResources.getDialogTitleResource(context, message); 598 CharSequence channelName = context.getText(channelTitleId); 599 String messageBody = message.getMessageBody(); 600 final NotificationManager notificationManager = 601 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 602 createNotificationChannels(context); 603 604 // Create intent to show the new messages when user selects the notification. 605 Intent intent; 606 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { 607 // For FEATURE_WATCH we want to mark as read 608 intent = createMarkAsReadIntent(context, message.getReceivedTime()); 609 } else { 610 // For anything else we handle it normally 611 intent = createDisplayMessageIntent(context, CellBroadcastAlertDialog.class, 612 messageList); 613 } 614 615 intent.putExtra(CellBroadcastAlertDialog.FROM_NOTIFICATION_EXTRA, true); 616 intent.putExtra(CellBroadcastAlertDialog.FROM_SAVE_STATE_NOTIFICATION_EXTRA, fromSaveState); 617 618 PendingIntent pi; 619 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { 620 pi = PendingIntent.getBroadcast(context, 0, intent, 0); 621 } else { 622 pi = PendingIntent.getActivity(context, NOTIFICATION_ID, intent, 623 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 624 } 625 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 626 context, message.getSubscriptionId()); 627 628 String channelId = channelManager.isEmergencyMessage(message) 629 ? NOTIFICATION_CHANNEL_EMERGENCY_ALERTS : NOTIFICATION_CHANNEL_NON_EMERGENCY_ALERTS; 630 if (channelId == NOTIFICATION_CHANNEL_EMERGENCY_ALERTS && sRemindAfterCallFinish) { 631 channelId = NOTIFICATION_CHANNEL_EMERGENCY_ALERTS_IN_VOICECALL; 632 } 633 634 boolean nonSwipeableNotification = message.isEmergencyMessage() 635 && CellBroadcastSettings.getResources(context, message.getSubscriptionId()) 636 .getBoolean(R.bool.non_swipeable_notification) || sRemindAfterCallFinish; 637 638 // use default sound/vibration/lights for non-emergency broadcasts 639 Notification.Builder builder = 640 new Notification.Builder(context, channelId) 641 .setSmallIcon(R.drawable.ic_warning_googred) 642 .setTicker(channelName) 643 .setWhen(System.currentTimeMillis()) 644 .setCategory(Notification.CATEGORY_SYSTEM) 645 .setPriority(Notification.PRIORITY_HIGH) 646 .setColor(res.getColor(R.color.notification_color)) 647 .setVisibility(Notification.VISIBILITY_PUBLIC) 648 .setOngoing(nonSwipeableNotification); 649 650 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { 651 builder.setDeleteIntent(pi); 652 // FEATURE_WATCH/CWH devices see this as priority 653 builder.setVibrate(new long[]{0}); 654 } else { 655 builder.setContentIntent(pi); 656 // This will break vibration on FEATURE_WATCH, so use it for anything else 657 builder.setDefaults(Notification.DEFAULT_ALL); 658 } 659 660 // increment unread alert count (decremented when user dismisses alert dialog) 661 int unreadCount = messageList.size(); 662 if (unreadCount > 1) { 663 // use generic count of unread broadcasts if more than one unread 664 builder.setContentTitle(context.getString(R.string.notification_multiple_title)); 665 builder.setContentText(context.getString(R.string.notification_multiple, unreadCount)); 666 } else { 667 builder.setContentTitle(channelName) 668 .setContentText(messageBody) 669 .setStyle(new Notification.BigTextStyle() 670 .bigText(messageBody)); 671 } 672 673 notificationManager.notify(NOTIFICATION_ID, builder.build()); 674 675 // FEATURE_WATCH devices do not have global sounds for notifications; only vibrate. 676 // TW requires sounds for 911/919 677 // Emergency messages use a different audio playback and display path. Since we use 678 // addToNotification for the emergency display on FEATURE WATCH devices vs the 679 // Alert Dialog, it will call this and override the emergency audio tone. 680 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) 681 && !channelManager.isEmergencyMessage(message)) { 682 if (res.getBoolean(R.bool.watch_enable_non_emergency_audio)) { 683 // start audio/vibration/speech service for non emergency alerts 684 Intent audioIntent = new Intent(context, CellBroadcastAlertAudio.class); 685 audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO); 686 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE, 687 AlertType.OTHER); 688 context.startService(audioIntent); 689 } 690 } 691 692 } 693 694 /** 695 * Creates the notification channel and registers it with NotificationManager. If a channel 696 * with the same ID is already registered, NotificationManager will ignore this call. 697 */ createNotificationChannels(Context context)698 static void createNotificationChannels(Context context) { 699 NotificationManager notificationManager = 700 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 701 notificationManager.createNotificationChannel( 702 new NotificationChannel( 703 NOTIFICATION_CHANNEL_EMERGENCY_ALERTS, 704 context.getString(R.string.notification_channel_emergency_alerts), 705 NotificationManager.IMPORTANCE_LOW)); 706 final NotificationChannel nonEmergency = new NotificationChannel( 707 NOTIFICATION_CHANNEL_NON_EMERGENCY_ALERTS, 708 context.getString(R.string.notification_channel_broadcast_messages), 709 NotificationManager.IMPORTANCE_DEFAULT); 710 nonEmergency.enableVibration(true); 711 notificationManager.createNotificationChannel(nonEmergency); 712 713 final NotificationChannel emergencyAlertInVoiceCall = new NotificationChannel( 714 NOTIFICATION_CHANNEL_EMERGENCY_ALERTS_IN_VOICECALL, 715 context.getString(R.string.notification_channel_broadcast_messages_in_voicecall), 716 NotificationManager.IMPORTANCE_HIGH); 717 emergencyAlertInVoiceCall.enableVibration(true); 718 notificationManager.createNotificationChannel(emergencyAlertInVoiceCall); 719 } 720 createDisplayMessageIntent(Context context, Class intentClass, ArrayList<SmsCbMessage> messageList)721 private static Intent createDisplayMessageIntent(Context context, Class intentClass, 722 ArrayList<SmsCbMessage> messageList) { 723 // Trigger the list activity to fire up a dialog that shows the received messages 724 Intent intent = new Intent(context, intentClass); 725 intent.putParcelableArrayListExtra(CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, 726 messageList); 727 return intent; 728 } 729 730 /** 731 * Creates a delete intent that calls to the {@link CellBroadcastReceiver} in order to mark 732 * a message as read 733 * 734 * @param context context of the caller 735 * @param deliveryTime time the message was sent in order to mark as read 736 * @return delete intent to add to the pending intent 737 */ createMarkAsReadIntent(Context context, long deliveryTime)738 static Intent createMarkAsReadIntent(Context context, long deliveryTime) { 739 Intent deleteIntent = new Intent(context, CellBroadcastReceiver.class); 740 deleteIntent.setAction(CellBroadcastReceiver.ACTION_MARK_AS_READ); 741 deleteIntent.putExtra(CellBroadcastReceiver.EXTRA_DELIVERY_TIME, deliveryTime); 742 return deleteIntent; 743 } 744 745 @VisibleForTesting 746 @Override onBind(Intent intent)747 public IBinder onBind(Intent intent) { 748 return new LocalBinder(); 749 } 750 751 @VisibleForTesting 752 class LocalBinder extends Binder { getService()753 public CellBroadcastAlertService getService() { 754 return CellBroadcastAlertService.this; 755 } 756 } 757 758 @Override onAudioFocusChange(int focusChange)759 public void onAudioFocusChange(int focusChange) { 760 if(focusChange == AudioManager.AUDIOFOCUS_GAIN) { 761 Log.d(TAG, "audio focus released from voice call, play pending alert if needed"); 762 mAudioManager.abandonAudioFocus(this); 763 playPendingAlert(); 764 } 765 } 766 767 /** 768 * Remove previous unread notifications and play stored unread 769 * emergency messages after voice call finish. 770 */ 771 private final PhoneStateListener mPhoneStateListener = new PhoneStateListener( 772 new Handler(Looper.getMainLooper())::post) { 773 @Override 774 public void onCallStateChanged(int state, String incomingNumber) { 775 776 switch (state) { 777 case TelephonyManager.CALL_STATE_IDLE: 778 Log.d(TAG, "onCallStateChanged: CALL_STATE_IDLE"); 779 // check if audio focus was released by voice call. This is to avoid possible 780 // race conditions that voice call did not release audio focus while alert is 781 // playing at the same time (out-of-rhythm) 782 if (mAudioManager == null) { 783 mAudioManager = (AudioManager) 784 getApplicationContext().getSystemService(Context.AUDIO_SERVICE); 785 } 786 int audioFocusResult = mAudioManager.requestAudioFocus( 787 CellBroadcastAlertService.this::onAudioFocusChange, 788 new AudioAttributes.Builder().setLegacyStreamType( 789 AudioManager.STREAM_ALARM).build(), 790 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, 791 AudioManager.AUDIOFOCUS_FLAG_DELAY_OK); 792 if (audioFocusResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 793 Log.d(TAG, "audio focus released from voice call, " 794 + "play pending alert if needed"); 795 mAudioManager.abandonAudioFocus( 796 CellBroadcastAlertService.this::onAudioFocusChange); 797 playPendingAlert(); 798 } else { 799 Log.d(TAG, "wait for audio focus release after call"); 800 } 801 break; 802 803 default: 804 Log.d(TAG, "onCallStateChanged: other state = " + state); 805 break; 806 } 807 } 808 }; 809 playPendingAlert()810 private void playPendingAlert() { 811 if (sRemindAfterCallFinish) { 812 sRemindAfterCallFinish = false; 813 NotificationManager notificationManager = (NotificationManager) 814 getApplicationContext().getSystemService( 815 Context.NOTIFICATION_SERVICE); 816 817 StatusBarNotification[] notificationList = 818 notificationManager.getActiveNotifications(); 819 820 if(notificationList != null && notificationList.length >0) { 821 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 822 ArrayList<SmsCbMessage> newMessageList = 823 CellBroadcastReceiverApp.getNewMessageList(); 824 825 for (int i = 0; i < newMessageList.size(); i++) { 826 openEmergencyAlertNotification(newMessageList.get(i)); 827 } 828 } 829 CellBroadcastReceiverApp.clearNewMessageList(); 830 } 831 } 832 833 /** 834 * Write displayed cellbroadcast messages to sms inbox 835 * 836 * @param message The cell broadcast message. 837 */ writeMessageToSmsInbox(@onNull SmsCbMessage message)838 private void writeMessageToSmsInbox(@NonNull SmsCbMessage message) { 839 // composing SMS 840 ContentValues cv = new ContentValues(); 841 cv.put(Telephony.Sms.Inbox.BODY, message.getMessageBody()); 842 cv.put(Telephony.Sms.Inbox.DATE, message.getReceivedTime()); 843 cv.put(Telephony.Sms.Inbox.SUBSCRIPTION_ID, message.getSubscriptionId()); 844 cv.put(Telephony.Sms.Inbox.SUBJECT, CellBroadcastResources.getDialogTitleResource(mContext, 845 message)); 846 cv.put(Telephony.Sms.Inbox.ADDRESS, mContext.getString(R.string.sms_cb_sender_name)); 847 // store all cellbroadcast messages in the same thread. 848 cv.put(Telephony.Sms.Inbox.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, 849 mContext.getString(R.string.sms_cb_sender_name))); 850 Uri uri = mContext.getContentResolver().insert(Telephony.Sms.Inbox.CONTENT_URI, cv); 851 if (uri == null) { 852 Log.e(TAG, "writeMessageToSmsInbox: failed"); 853 } else { 854 Log.d(TAG, "writeMessageToSmsInbox: succeed uri = " + uri); 855 } 856 } 857 } 858