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