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