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