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