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