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