CellBroadcastAlertService.java revision 68dc486fc39a727c43199c2df72dcf658367a63f
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.content.Context; 25import android.content.Intent; 26import android.content.SharedPreferences; 27import android.os.Bundle; 28import android.os.IBinder; 29import android.os.PowerManager; 30import android.preference.PreferenceManager; 31import android.provider.Telephony; 32import android.telephony.CellBroadcastMessage; 33import android.telephony.SmsCbCmasInfo; 34import android.telephony.SmsCbLocation; 35import android.telephony.SmsCbMessage; 36import android.util.Log; 37 38import java.util.HashSet; 39 40/** 41 * This service manages the display and animation of broadcast messages. 42 * Emergency messages display with a flashing animated exclamation mark icon, 43 * and an alert tone is played when the alert is first shown to the user 44 * (but not when the user views a previously received broadcast). 45 */ 46public class CellBroadcastAlertService extends Service { 47 private static final String TAG = "CellBroadcastAlertService"; 48 49 /** Identifier for notification ID extra. */ 50 public static final String SMS_CB_NOTIFICATION_ID_EXTRA = 51 "com.android.cellbroadcastreceiver.SMS_CB_NOTIFICATION_ID"; 52 53 /** Intent extra to indicate a previously unread alert. */ 54 static final String NEW_ALERT_EXTRA = "com.android.cellbroadcastreceiver.NEW_ALERT"; 55 56 /** Intent action to display alert dialog/notification, after verifying the alert is new. */ 57 static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT"; 58 59 /** Use the same notification ID for non-emergency alerts. */ 60 static final int NOTIFICATION_ID = 1; 61 62 /** CPU wake lock while handling emergency alert notification. */ 63 private PowerManager.WakeLock mWakeLock; 64 65 /** Hold the wake lock for 5 seconds, which should be enough time to display the alert. */ 66 private static final int WAKE_LOCK_TIMEOUT = 5000; 67 68 /** Container for message ID and geographical scope, for duplicate message detection. */ 69 private static final class MessageIdAndScope { 70 private final int mMessageId; 71 private final SmsCbLocation mLocation; 72 73 MessageIdAndScope(int messageId, SmsCbLocation location) { 74 mMessageId = messageId; 75 mLocation = location; 76 } 77 78 @Override 79 public int hashCode() { 80 return mMessageId * 31 + mLocation.hashCode(); 81 } 82 83 @Override 84 public boolean equals(Object o) { 85 if (o == this) { 86 return true; 87 } 88 if (o instanceof MessageIdAndScope) { 89 MessageIdAndScope other = (MessageIdAndScope) o; 90 return (mMessageId == other.mMessageId && mLocation.equals(other.mLocation)); 91 } 92 return false; 93 } 94 95 @Override 96 public String toString() { 97 return "{messageId: " + mMessageId + " location: " + mLocation.toString() + '}'; 98 } 99 } 100 101 /** Cache of received message IDs, for duplicate message detection. */ 102 private static final HashSet<MessageIdAndScope> sCmasIdList = new HashSet<MessageIdAndScope>(8); 103 104 @Override 105 public int onStartCommand(Intent intent, int flags, int startId) { 106 String action = intent.getAction(); 107 if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) || 108 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) { 109 handleCellBroadcastIntent(intent); 110 } else if (SHOW_NEW_ALERT_ACTION.equals(action)) { 111 showNewAlert(intent); 112 } else { 113 Log.e(TAG, "Unrecognized intent action: " + action); 114 } 115 return START_NOT_STICKY; 116 } 117 118 private void handleCellBroadcastIntent(Intent intent) { 119 Bundle extras = intent.getExtras(); 120 if (extras == null) { 121 Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!"); 122 return; 123 } 124 125 SmsCbMessage message = (SmsCbMessage) extras.get("message"); 126 127 if (message == null) { 128 Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra"); 129 return; 130 } 131 132 final CellBroadcastMessage cbm = new CellBroadcastMessage(message); 133 if (!isMessageEnabledByUser(cbm)) { 134 Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() + 135 " by user preference"); 136 return; 137 } 138 139 // Set.add() returns false if message ID has already been added 140 MessageIdAndScope messageIdAndScope = new MessageIdAndScope(message.getSerialNumber(), 141 message.getLocation()); 142 if (!sCmasIdList.add(messageIdAndScope)) { 143 Log.d(TAG, "ignoring duplicate alert with " + messageIdAndScope); 144 return; 145 } 146 147 final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION); 148 alertIntent.setClass(this, CellBroadcastAlertService.class); 149 alertIntent.putExtra("message", cbm); 150 151 // write to database on a background thread 152 new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver()) 153 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() { 154 @Override 155 public boolean execute(CellBroadcastContentProvider provider) { 156 if (provider.insertNewBroadcast(cbm)) { 157 // new message, show the alert or notification on UI thread 158 startService(alertIntent); 159 return true; 160 } else { 161 return false; 162 } 163 } 164 }); 165 } 166 167 private void showNewAlert(Intent intent) { 168 Bundle extras = intent.getExtras(); 169 if (extras == null) { 170 Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!"); 171 return; 172 } 173 174 CellBroadcastMessage cbm = (CellBroadcastMessage) extras.get("message"); 175 176 if (cbm == null) { 177 Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra"); 178 return; 179 } 180 181 if (cbm.isEmergencyAlertMessage() || CellBroadcastConfigService 182 .isOperatorDefinedEmergencyId(cbm.getServiceCategory())) { 183 // start alert sound / vibration / TTS and display full-screen alert 184 openEmergencyAlertNotification(cbm); 185 } else { 186 // add notification to the bar 187 addToNotificationBar(cbm); 188 } 189 } 190 191 /** 192 * Filter out broadcasts on the test channels that the user has not enabled, 193 * and types of notifications that the user is not interested in receiving. 194 * This allows us to enable an entire range of message identifiers in the 195 * radio and not have to explicitly disable the message identifiers for 196 * test broadcasts. In the unlikely event that the default shared preference 197 * values were not initialized in CellBroadcastReceiverApp, the second parameter 198 * to the getBoolean() calls match the default values in res/xml/preferences.xml. 199 * 200 * @param message the message to check 201 * @return true if the user has enabled this message type; false otherwise 202 */ 203 private boolean isMessageEnabledByUser(CellBroadcastMessage message) { 204 if (message.isEtwsTestMessage()) { 205 return PreferenceManager.getDefaultSharedPreferences(this) 206 .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false); 207 } 208 209 if (message.isCmasMessage()) { 210 switch (message.getCmasMessageClass()) { 211 case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT: 212 return PreferenceManager.getDefaultSharedPreferences(this).getBoolean( 213 CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true); 214 215 case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT: 216 return PreferenceManager.getDefaultSharedPreferences(this).getBoolean( 217 CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true); 218 219 case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY: 220 return PreferenceManager.getDefaultSharedPreferences(this) 221 .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true); 222 223 case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST: 224 case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE: 225 return PreferenceManager.getDefaultSharedPreferences(this) 226 .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, false); 227 228 default: 229 return true; // presidential-level CMAS alerts are always enabled 230 } 231 } 232 233 return true; // other broadcast messages are always enabled 234 } 235 236 private void acquireTimedWakelock(int timeout) { 237 if (mWakeLock == null) { 238 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 239 // Note: acquiring a PARTIAL_WAKE_LOCK and setting window flag FLAG_TURN_SCREEN_ON in 240 // CellBroadcastAlertFullScreen is not sufficient to turn on the screen by itself. 241 // Use SCREEN_BRIGHT_WAKE_LOCK here as a workaround to ensure the screen turns on. 242 mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK 243 | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); 244 } 245 mWakeLock.acquire(timeout); 246 } 247 248 /** 249 * Display a full-screen alert message for emergency alerts. 250 * @param message the alert to display 251 */ 252 private void openEmergencyAlertNotification(CellBroadcastMessage message) { 253 // Acquire a CPU wake lock until the alert dialog and audio start playing. 254 acquireTimedWakelock(WAKE_LOCK_TIMEOUT); 255 256 // Close dialogs and window shade 257 Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 258 sendBroadcast(closeDialogs); 259 260 // start audio/vibration/speech service for emergency alerts 261 Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class); 262 audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO); 263 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 264 265 int duration; // alert audio duration in ms 266 if (message.isCmasMessage()) { 267 // CMAS requirement: duration of the audio attention signal is 10.5 seconds. 268 duration = 10500; 269 } else { 270 duration = Integer.parseInt(prefs.getString( 271 CellBroadcastSettings.KEY_ALERT_SOUND_DURATION, 272 CellBroadcastSettings.ALERT_SOUND_DEFAULT_DURATION)) * 1000; 273 } 274 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION_EXTRA, duration); 275 276 if (message.isEtwsMessage()) { 277 // For ETWS, always vibrate, even in silent mode. 278 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA, true); 279 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_ETWS_VIBRATE_EXTRA, true); 280 } else { 281 // For other alerts, vibration can be disabled in app settings. 282 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA, 283 prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true)); 284 } 285 286 int channelTitleId = CellBroadcastResources.getDialogTitleResource(message); 287 CharSequence channelName = getText(channelTitleId); 288 String messageBody = message.getMessageBody(); 289 290 if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) { 291 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody); 292 293 String language = message.getLanguageCode(); 294 if (message.isEtwsMessage() && !"ja".equals(language)) { 295 Log.w(TAG, "bad language code for ETWS - using Japanese TTS"); 296 language = "ja"; 297 } else if (message.isCmasMessage() && !"en".equals(language)) { 298 Log.w(TAG, "bad language code for CMAS - using English TTS"); 299 language = "en"; 300 } 301 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE, 302 language); 303 } 304 startService(audioIntent); 305 306 // Use lower 32 bits of emergency alert delivery time for notification ID 307 int notificationId = (int) message.getDeliveryTime(); 308 309 // Decide which activity to start based on the state of the keyguard. 310 Class c = CellBroadcastAlertDialog.class; 311 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 312 if (km.inKeyguardRestrictedInputMode()) { 313 // Use the full screen activity for security. 314 c = CellBroadcastAlertFullScreen.class; 315 } 316 317 Intent notify = createDisplayMessageIntent(this, c, message, notificationId); 318 PendingIntent pi = PendingIntent.getActivity(this, notificationId, notify, 0); 319 320 Notification.Builder builder = new Notification.Builder(this) 321 .setSmallIcon(R.drawable.ic_notify_alert) 322 .setTicker(getText(CellBroadcastResources.getDialogTitleResource(message))) 323 .setWhen(System.currentTimeMillis()) 324 .setContentIntent(pi) 325 .setFullScreenIntent(pi, true) 326 .setContentTitle(channelName) 327 .setContentText(messageBody) 328 .setDefaults(Notification.DEFAULT_LIGHTS); 329 330 NotificationManager notificationManager = 331 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); 332 333 notificationManager.notify(notificationId, builder.getNotification()); 334 } 335 336 /** 337 * Add the new alert to the notification bar (non-emergency alerts), or launch a 338 * high-priority immediate intent for emergency alerts. 339 * @param message the alert to display 340 */ 341 private void addToNotificationBar(CellBroadcastMessage message) { 342 int channelTitleId = CellBroadcastResources.getDialogTitleResource(message); 343 CharSequence channelName = getText(channelTitleId); 344 String messageBody = message.getMessageBody(); 345 346 // Use the same ID to create a single notification for multiple non-emergency alerts. 347 int notificationId = NOTIFICATION_ID; 348 349 PendingIntent pi = PendingIntent.getActivity(this, 0, createDisplayMessageIntent( 350 this, CellBroadcastListActivity.class, message, notificationId), 0); 351 352 // use default sound/vibration/lights for non-emergency broadcasts 353 Notification.Builder builder = new Notification.Builder(this) 354 .setSmallIcon(R.drawable.ic_notify_alert) 355 .setTicker(channelName) 356 .setWhen(System.currentTimeMillis()) 357 .setContentIntent(pi) 358 .setDefaults(Notification.DEFAULT_ALL); 359 360 builder.setDefaults(Notification.DEFAULT_ALL); 361 362 // increment unread alert count (decremented when user dismisses alert dialog) 363 int unreadCount = CellBroadcastReceiverApp.incrementUnreadAlertCount(); 364 if (unreadCount > 1) { 365 // use generic count of unread broadcasts if more than one unread 366 builder.setContentTitle(getString(R.string.notification_multiple_title)); 367 builder.setContentText(getString(R.string.notification_multiple, unreadCount)); 368 } else { 369 builder.setContentTitle(channelName).setContentText(messageBody); 370 } 371 372 Log.i(TAG, "addToNotificationBar notificationId: " + notificationId); 373 374 NotificationManager notificationManager = 375 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); 376 377 notificationManager.notify(notificationId, builder.getNotification()); 378 } 379 380 static Intent createDisplayMessageIntent(Context context, Class intentClass, 381 CellBroadcastMessage message, int notificationId) { 382 // Trigger the list activity to fire up a dialog that shows the received messages 383 Intent intent = new Intent(context, intentClass); 384 intent.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, message); 385 intent.putExtra(SMS_CB_NOTIFICATION_ID_EXTRA, notificationId); 386 intent.putExtra(NEW_ALERT_EXTRA, true); 387 388 // This line is needed to make this intent compare differently than the other intents 389 // created here for other messages. Without this line, the PendingIntent always gets the 390 // intent of a previous message and notification. 391 intent.setType(Integer.toString(notificationId)); 392 393 return intent; 394 } 395 396 @Override 397 public IBinder onBind(Intent intent) { 398 return null; // clients can't bind to this service 399 } 400} 401