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