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