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