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