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