CellBroadcastAlertDialog.java revision 468a41702543cb74189153a79af5b38a344e9960
1/* 2 * Copyright (C) 2016 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.Activity; 20import android.app.KeyguardManager; 21import android.app.NotificationManager; 22import android.content.Context; 23import android.content.Intent; 24import android.content.SharedPreferences; 25import android.content.res.Resources; 26import android.graphics.drawable.Drawable; 27import android.os.Bundle; 28import android.os.Handler; 29import android.os.Message; 30import android.os.PowerManager; 31import android.preference.PreferenceManager; 32import android.provider.Telephony; 33import android.telephony.CellBroadcastMessage; 34import android.telephony.SmsCbCmasInfo; 35import android.util.Log; 36import android.view.KeyEvent; 37import android.view.LayoutInflater; 38import android.view.View; 39import android.view.Window; 40import android.view.WindowManager; 41import android.widget.Button; 42import android.widget.ImageView; 43import android.widget.TextView; 44 45import java.util.ArrayList; 46import java.util.concurrent.atomic.AtomicInteger; 47 48/** 49 * Custom alert dialog with optional flashing warning icon. 50 * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}. 51 */ 52public class CellBroadcastAlertDialog extends Activity { 53 54 private static final String TAG = "CellBroadcastAlertDialog"; 55 56 /** Intent extra for non-emergency alerts sent when user selects the notification. */ 57 static final String FROM_NOTIFICATION_EXTRA = "from_notification"; 58 59 // Intent extra to identify if notification was sent while trying to move away from the dialog 60 // without acknowleding the dialog 61 static final String FROM_SAVE_STATE_NOTIFICATION_EXTRA = "from_save_state_notification"; 62 63 /** List of cell broadcast messages to display (oldest to newest). */ 64 protected ArrayList<CellBroadcastMessage> mMessageList; 65 66 /** Whether a CMAS alert other than Presidential Alert was displayed. */ 67 private boolean mShowOptOutDialog; 68 69 /** Length of time for the warning icon to be visible. */ 70 private static final int WARNING_ICON_ON_DURATION_MSEC = 800; 71 72 /** Length of time for the warning icon to be off. */ 73 private static final int WARNING_ICON_OFF_DURATION_MSEC = 800; 74 75 /** Length of time to keep the screen turned on. */ 76 private static final int KEEP_SCREEN_ON_DURATION_MSEC = 60000; 77 78 /** Animation handler for the flashing warning icon (emergency alerts only). */ 79 private final AnimationHandler mAnimationHandler = new AnimationHandler(); 80 81 /** Handler to add and remove screen on flags for emergency alerts. */ 82 private final ScreenOffHandler mScreenOffHandler = new ScreenOffHandler(); 83 84 /** 85 * Animation handler for the flashing warning icon (emergency alerts only). 86 */ 87 private class AnimationHandler extends Handler { 88 /** Latest {@code message.what} value for detecting old messages. */ 89 private final AtomicInteger mCount = new AtomicInteger(); 90 91 /** Warning icon state: visible == true, hidden == false. */ 92 private boolean mWarningIconVisible; 93 94 /** The warning icon Drawable. */ 95 private Drawable mWarningIcon; 96 97 /** The View containing the warning icon. */ 98 private ImageView mWarningIconView; 99 100 /** Package local constructor (called from outer class). */ 101 AnimationHandler() {} 102 103 /** Start the warning icon animation. */ 104 void startIconAnimation() { 105 if (!initDrawableAndImageView()) { 106 return; // init failure 107 } 108 mWarningIconVisible = true; 109 mWarningIconView.setVisibility(View.VISIBLE); 110 updateIconState(); 111 queueAnimateMessage(); 112 } 113 114 /** Stop the warning icon animation. */ 115 void stopIconAnimation() { 116 // Increment the counter so the handler will ignore the next message. 117 mCount.incrementAndGet(); 118 if (mWarningIconView != null) { 119 mWarningIconView.setVisibility(View.GONE); 120 } 121 } 122 123 /** Update the visibility of the warning icon. */ 124 private void updateIconState() { 125 mWarningIconView.setImageAlpha(mWarningIconVisible ? 255 : 0); 126 mWarningIconView.invalidateDrawable(mWarningIcon); 127 } 128 129 /** Queue a message to animate the warning icon. */ 130 private void queueAnimateMessage() { 131 int msgWhat = mCount.incrementAndGet(); 132 sendEmptyMessageDelayed(msgWhat, mWarningIconVisible ? WARNING_ICON_ON_DURATION_MSEC 133 : WARNING_ICON_OFF_DURATION_MSEC); 134 } 135 136 @Override 137 public void handleMessage(Message msg) { 138 if (msg.what == mCount.get()) { 139 mWarningIconVisible = !mWarningIconVisible; 140 updateIconState(); 141 queueAnimateMessage(); 142 } 143 } 144 145 /** 146 * Initialize the Drawable and ImageView fields. 147 * @return true if successful; false if any field failed to initialize 148 */ 149 private boolean initDrawableAndImageView() { 150 if (mWarningIcon == null) { 151 try { 152 mWarningIcon = getResources().getDrawable(R.drawable.ic_warning_large); 153 } catch (Resources.NotFoundException e) { 154 Log.e(TAG, "warning icon resource not found", e); 155 return false; 156 } 157 } 158 if (mWarningIconView == null) { 159 mWarningIconView = (ImageView) findViewById(R.id.icon); 160 if (mWarningIconView != null) { 161 mWarningIconView.setImageDrawable(mWarningIcon); 162 } else { 163 Log.e(TAG, "failed to get ImageView for warning icon"); 164 return false; 165 } 166 } 167 return true; 168 } 169 } 170 171 /** 172 * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay, 173 * remove the flag so the screen can turn off to conserve the battery. 174 */ 175 private class ScreenOffHandler extends Handler { 176 /** Latest {@code message.what} value for detecting old messages. */ 177 private final AtomicInteger mCount = new AtomicInteger(); 178 179 /** Package local constructor (called from outer class). */ 180 ScreenOffHandler() {} 181 182 /** Add screen on window flags and queue a delayed message to remove them later. */ 183 void startScreenOnTimer() { 184 addWindowFlags(); 185 int msgWhat = mCount.incrementAndGet(); 186 removeMessages(msgWhat - 1); // Remove previous message, if any. 187 sendEmptyMessageDelayed(msgWhat, KEEP_SCREEN_ON_DURATION_MSEC); 188 Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat); 189 } 190 191 /** Remove the screen on window flags and any queued screen off message. */ 192 void stopScreenOnTimer() { 193 removeMessages(mCount.get()); 194 clearWindowFlags(); 195 } 196 197 /** Set the screen on window flags. */ 198 private void addWindowFlags() { 199 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 200 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 201 } 202 203 /** Clear the screen on window flags. */ 204 private void clearWindowFlags() { 205 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 206 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 207 } 208 209 @Override 210 public void handleMessage(Message msg) { 211 int msgWhat = msg.what; 212 if (msgWhat == mCount.get()) { 213 clearWindowFlags(); 214 Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat); 215 } else { 216 Log.e(TAG, "discarding screen off message with id " + msgWhat); 217 } 218 } 219 } 220 221 @Override 222 protected void onCreate(Bundle savedInstanceState) { 223 super.onCreate(savedInstanceState); 224 225 final Window win = getWindow(); 226 227 // We use a custom title, so remove the standard dialog title bar 228 win.requestFeature(Window.FEATURE_NO_TITLE); 229 230 // Full screen alerts display above the keyguard and when device is locked. 231 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN 232 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 233 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 234 235 setFinishOnTouchOutside(false); 236 237 // Initialize the view. 238 LayoutInflater inflater = LayoutInflater.from(this); 239 setContentView(inflater.inflate(R.layout.cell_broadcast_alert, null)); 240 241 findViewById(R.id.dismissButton).setOnClickListener( 242 new Button.OnClickListener() { 243 @Override 244 public void onClick(View v) { 245 dismiss(); 246 } 247 }); 248 249 // Get message list from saved Bundle or from Intent. 250 if (savedInstanceState != null) { 251 Log.d(TAG, "onCreate getting message list from saved instance state"); 252 mMessageList = savedInstanceState.getParcelableArrayList( 253 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); 254 } else { 255 Log.d(TAG, "onCreate getting message list from intent"); 256 Intent intent = getIntent(); 257 mMessageList = intent.getParcelableArrayListExtra( 258 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); 259 260 // If we were started from a notification, dismiss it. 261 clearNotification(intent); 262 } 263 264 if (mMessageList == null || mMessageList.size() == 0) { 265 Log.e(TAG, "onCreate failed as message list is null or empty"); 266 finish(); 267 } else { 268 Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size()); 269 } 270 271 // For emergency alerts, keep screen on so the user can read it 272 CellBroadcastMessage message = getLatestMessage(); 273 if (message != null && message.isEmergencyAlertMessage()) { 274 Log.d(TAG, "onCreate setting screen on timer for emergency alert"); 275 mScreenOffHandler.startScreenOnTimer(); 276 } 277 278 updateAlertText(message); 279 } 280 281 /** 282 * Start animating warning icon. 283 */ 284 @Override 285 protected void onResume() { 286 super.onResume(); 287 CellBroadcastMessage message = getLatestMessage(); 288 if (message != null && message.isEmergencyAlertMessage()) { 289 mAnimationHandler.startIconAnimation(); 290 } 291 } 292 293 /** 294 * Stop animating warning icon. 295 */ 296 @Override 297 protected void onPause() { 298 Log.d(TAG, "onPause called"); 299 mAnimationHandler.stopIconAnimation(); 300 super.onPause(); 301 } 302 303 @Override 304 protected void onStop() { 305 super.onStop(); 306 // When the activity goes in background eg. clicking Home button, send notification. 307 // Avoid doing this when activity will be recreated because of orientation change or if 308 // screen goes off 309 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 310 if (!(isChangingConfigurations() || getLatestMessage() == null) && pm.isScreenOn()) { 311 CellBroadcastAlertService.addToNotificationBar(getLatestMessage(), mMessageList, 312 getApplicationContext(), true); 313 } 314 } 315 316 /** Returns the currently displayed message. */ 317 CellBroadcastMessage getLatestMessage() { 318 int index = mMessageList.size() - 1; 319 if (index >= 0) { 320 return mMessageList.get(index); 321 } else { 322 Log.d(TAG, "getLatestMessage returns null"); 323 return null; 324 } 325 } 326 327 /** Removes and returns the currently displayed message. */ 328 private CellBroadcastMessage removeLatestMessage() { 329 int index = mMessageList.size() - 1; 330 if (index >= 0) { 331 return mMessageList.remove(index); 332 } else { 333 return null; 334 } 335 } 336 337 /** 338 * Save the list of messages so the state can be restored later. 339 * @param outState Bundle in which to place the saved state. 340 */ 341 @Override 342 protected void onSaveInstanceState(Bundle outState) { 343 super.onSaveInstanceState(outState); 344 outState.putParcelableArrayList(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessageList); 345 } 346 347 /** 348 * Update alert text when a new emergency alert arrives. 349 * @param message CB message which is used to update alert text. 350 */ 351 private void updateAlertText(CellBroadcastMessage message) { 352 int titleId = CellBroadcastResources.getDialogTitleResource(message); 353 setTitle(titleId); 354 ((TextView) findViewById(R.id.alertTitle)).setText(titleId); 355 ((TextView) findViewById(R.id.message)).setText(message.getMessageBody()); 356 357 // Set alert reminder depending on user preference 358 CellBroadcastAlertReminder.queueAlertReminder(this, true); 359 } 360 361 /** 362 * Called by {@link CellBroadcastAlertService} to add a new alert to the stack. 363 * @param intent The new intent containing one or more {@link CellBroadcastMessage}s. 364 */ 365 @Override 366 protected void onNewIntent(Intent intent) { 367 ArrayList<CellBroadcastMessage> newMessageList = intent.getParcelableArrayListExtra( 368 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); 369 if (newMessageList != null) { 370 if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) { 371 mMessageList = newMessageList; 372 } else { 373 mMessageList.addAll(newMessageList); 374 } 375 Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size()); 376 updateAlertText(getLatestMessage()); 377 // If the new intent was sent from a notification, dismiss it. 378 clearNotification(intent); 379 } else { 380 Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring"); 381 } 382 } 383 384 /** 385 * Try to cancel any notification that may have started this activity. 386 * @param intent Intent containing extras used to identify if notification needs to be cleared 387 */ 388 private void clearNotification(Intent intent) { 389 if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) { 390 NotificationManager notificationManager = 391 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 392 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 393 CellBroadcastReceiverApp.clearNewMessageList(); 394 } 395 } 396 397 /** 398 * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio} 399 * service if necessary. 400 */ 401 void dismiss() { 402 Log.d(TAG, "dismiss"); 403 // Stop playing alert sound/vibration/speech (if started) 404 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 405 406 // Cancel any pending alert reminder 407 CellBroadcastAlertReminder.cancelAlertReminder(); 408 409 // Remove the current alert message from the list. 410 CellBroadcastMessage lastMessage = removeLatestMessage(); 411 if (lastMessage == null) { 412 Log.e(TAG, "dismiss() called with empty message list!"); 413 finish(); 414 return; 415 } 416 417 // Mark the alert as read. 418 final long deliveryTime = lastMessage.getDeliveryTime(); 419 420 // Mark broadcast as read on a background thread. 421 new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver()) 422 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() { 423 @Override 424 public boolean execute(CellBroadcastContentProvider provider) { 425 return provider.markBroadcastRead( 426 Telephony.CellBroadcasts.DELIVERY_TIME, deliveryTime); 427 } 428 }); 429 430 // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert). 431 if (lastMessage.isCmasMessage() && lastMessage.getCmasMessageClass() != 432 SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) { 433 mShowOptOutDialog = true; 434 } 435 436 // If there are older emergency alerts to display, update the alert text and return. 437 CellBroadcastMessage nextMessage = getLatestMessage(); 438 if (nextMessage != null) { 439 updateAlertText(nextMessage); 440 if (nextMessage.isEmergencyAlertMessage()) { 441 mAnimationHandler.startIconAnimation(); 442 } else { 443 mAnimationHandler.stopIconAnimation(); 444 } 445 return; 446 } 447 448 // Remove pending screen-off messages (animation messages are removed in onPause()). 449 mScreenOffHandler.stopScreenOnTimer(); 450 451 // Show opt-in/opt-out dialog when the first CMAS alert is received. 452 if (mShowOptOutDialog) { 453 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 454 if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) { 455 // Clear the flag so the user will only see the opt-out dialog once. 456 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false) 457 .apply(); 458 459 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 460 if (km.inKeyguardRestrictedInputMode()) { 461 Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)"); 462 Intent intent = new Intent(this, CellBroadcastOptOutActivity.class); 463 startActivity(intent); 464 } else { 465 Log.d(TAG, "Showing opt-out dialog in current activity"); 466 CellBroadcastOptOutActivity.showOptOutDialog(this); 467 return; // don't call finish() until user dismisses the dialog 468 } 469 } 470 } 471 NotificationManager notificationManager = 472 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 473 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 474 finish(); 475 } 476 477 @Override 478 public boolean dispatchKeyEvent(KeyEvent event) { 479 CellBroadcastMessage message = getLatestMessage(); 480 if (message != null && !message.isEtwsMessage()) { 481 switch (event.getKeyCode()) { 482 // Volume keys and camera keys mute the alert sound/vibration (except ETWS). 483 case KeyEvent.KEYCODE_VOLUME_UP: 484 case KeyEvent.KEYCODE_VOLUME_DOWN: 485 case KeyEvent.KEYCODE_VOLUME_MUTE: 486 case KeyEvent.KEYCODE_CAMERA: 487 case KeyEvent.KEYCODE_FOCUS: 488 // Stop playing alert sound/vibration/speech (if started) 489 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 490 return true; 491 492 default: 493 break; 494 } 495 } 496 return super.dispatchKeyEvent(event); 497 } 498 499 @Override 500 public void onBackPressed() { 501 // Disable back key 502 } 503} 504