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 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.os.PowerManager; 31 import android.preference.PreferenceManager; 32 import android.provider.Telephony; 33 import android.telephony.CellBroadcastMessage; 34 import android.telephony.SmsCbCmasInfo; 35 import android.util.Log; 36 import android.view.KeyEvent; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.Window; 40 import android.view.WindowManager; 41 import android.widget.Button; 42 import android.widget.ImageView; 43 import android.widget.TextView; 44 45 import java.util.ArrayList; 46 import 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 */ 52 public 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 acknowledging 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). */ AnimationHandler()101 AnimationHandler() {} 102 103 /** Start the warning icon animation. */ startIconAnimation()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. */ stopIconAnimation()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. */ updateIconState()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. */ queueAnimateMessage()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 handleMessage(Message msg)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 */ initDrawableAndImageView()149 private boolean initDrawableAndImageView() { 150 if (mWarningIcon == null) { 151 try { 152 mWarningIcon = CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId( 153 getApplicationContext()).getDrawable(R.drawable.ic_warning_googred); 154 } catch (Resources.NotFoundException e) { 155 Log.e(TAG, "warning icon resource not found", e); 156 return false; 157 } 158 } 159 if (mWarningIconView == null) { 160 mWarningIconView = (ImageView) findViewById(R.id.icon); 161 if (mWarningIconView != null) { 162 mWarningIconView.setImageDrawable(mWarningIcon); 163 } else { 164 Log.e(TAG, "failed to get ImageView for warning icon"); 165 return false; 166 } 167 } 168 return true; 169 } 170 } 171 172 /** 173 * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay, 174 * remove the flag so the screen can turn off to conserve the battery. 175 */ 176 private class ScreenOffHandler extends Handler { 177 /** Latest {@code message.what} value for detecting old messages. */ 178 private final AtomicInteger mCount = new AtomicInteger(); 179 180 /** Package local constructor (called from outer class). */ ScreenOffHandler()181 ScreenOffHandler() {} 182 183 /** Add screen on window flags and queue a delayed message to remove them later. */ startScreenOnTimer()184 void startScreenOnTimer() { 185 addWindowFlags(); 186 int msgWhat = mCount.incrementAndGet(); 187 removeMessages(msgWhat - 1); // Remove previous message, if any. 188 sendEmptyMessageDelayed(msgWhat, KEEP_SCREEN_ON_DURATION_MSEC); 189 Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat); 190 } 191 192 /** Remove the screen on window flags and any queued screen off message. */ stopScreenOnTimer()193 void stopScreenOnTimer() { 194 removeMessages(mCount.get()); 195 clearWindowFlags(); 196 } 197 198 /** Set the screen on window flags. */ addWindowFlags()199 private void addWindowFlags() { 200 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 201 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 202 } 203 204 /** Clear the screen on window flags. */ clearWindowFlags()205 private void clearWindowFlags() { 206 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 207 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 208 } 209 210 @Override handleMessage(Message msg)211 public void handleMessage(Message msg) { 212 int msgWhat = msg.what; 213 if (msgWhat == mCount.get()) { 214 clearWindowFlags(); 215 Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat); 216 } else { 217 Log.e(TAG, "discarding screen off message with id " + msgWhat); 218 } 219 } 220 } 221 222 @Override onCreate(Bundle savedInstanceState)223 protected void onCreate(Bundle savedInstanceState) { 224 super.onCreate(savedInstanceState); 225 226 final Window win = getWindow(); 227 228 // We use a custom title, so remove the standard dialog title bar 229 win.requestFeature(Window.FEATURE_NO_TITLE); 230 231 // Full screen alerts display above the keyguard and when device is locked. 232 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN 233 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 234 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 235 236 setFinishOnTouchOutside(false); 237 238 // Initialize the view. 239 LayoutInflater inflater = LayoutInflater.from(this); 240 setContentView(inflater.inflate(R.layout.cell_broadcast_alert, null)); 241 242 findViewById(R.id.dismissButton).setOnClickListener( 243 new Button.OnClickListener() { 244 @Override 245 public void onClick(View v) { 246 dismiss(); 247 } 248 }); 249 250 // Get message list from saved Bundle or from Intent. 251 if (savedInstanceState != null) { 252 Log.d(TAG, "onCreate getting message list from saved instance state"); 253 mMessageList = savedInstanceState.getParcelableArrayList( 254 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); 255 } else { 256 Log.d(TAG, "onCreate getting message list from intent"); 257 Intent intent = getIntent(); 258 mMessageList = intent.getParcelableArrayListExtra( 259 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); 260 261 // If we were started from a notification, dismiss it. 262 clearNotification(intent); 263 } 264 265 if (mMessageList == null || mMessageList.size() == 0) { 266 Log.e(TAG, "onCreate failed as message list is null or empty"); 267 finish(); 268 } else { 269 Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size()); 270 } 271 272 // For emergency alerts, keep screen on so the user can read it 273 CellBroadcastMessage message = getLatestMessage(); 274 if (message != null && CellBroadcastChannelManager.isEmergencyMessage( 275 this, message)) { 276 Log.d(TAG, "onCreate setting screen on timer for emergency alert"); 277 mScreenOffHandler.startScreenOnTimer(); 278 } 279 280 updateAlertText(message); 281 } 282 283 /** 284 * Start animating warning icon. 285 */ 286 @Override onResume()287 protected void onResume() { 288 super.onResume(); 289 CellBroadcastMessage message = getLatestMessage(); 290 if (message != null && CellBroadcastChannelManager.isEmergencyMessage(this, message)) { 291 mAnimationHandler.startIconAnimation(); 292 } 293 } 294 295 /** 296 * Stop animating warning icon. 297 */ 298 @Override onPause()299 protected void onPause() { 300 Log.d(TAG, "onPause called"); 301 mAnimationHandler.stopIconAnimation(); 302 super.onPause(); 303 } 304 305 @Override onStop()306 protected void onStop() { 307 super.onStop(); 308 // When the activity goes in background eg. clicking Home button, send notification. 309 // Avoid doing this when activity will be recreated because of orientation change or if 310 // screen goes off 311 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 312 if (!(isChangingConfigurations() || getLatestMessage() == null) && pm.isScreenOn()) { 313 CellBroadcastAlertService.addToNotificationBar(getLatestMessage(), mMessageList, 314 getApplicationContext(), true); 315 } 316 } 317 318 /** Returns the currently displayed message. */ getLatestMessage()319 CellBroadcastMessage getLatestMessage() { 320 int index = mMessageList.size() - 1; 321 if (index >= 0) { 322 return mMessageList.get(index); 323 } else { 324 Log.d(TAG, "getLatestMessage returns null"); 325 return null; 326 } 327 } 328 329 /** Removes and returns the currently displayed message. */ removeLatestMessage()330 private CellBroadcastMessage removeLatestMessage() { 331 int index = mMessageList.size() - 1; 332 if (index >= 0) { 333 return mMessageList.remove(index); 334 } else { 335 return null; 336 } 337 } 338 339 /** 340 * Save the list of messages so the state can be restored later. 341 * @param outState Bundle in which to place the saved state. 342 */ 343 @Override onSaveInstanceState(Bundle outState)344 protected void onSaveInstanceState(Bundle outState) { 345 super.onSaveInstanceState(outState); 346 outState.putParcelableArrayList(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessageList); 347 } 348 349 /** 350 * Update alert text when a new emergency alert arrives. 351 * @param message CB message which is used to update alert text. 352 */ updateAlertText(CellBroadcastMessage message)353 private void updateAlertText(CellBroadcastMessage message) { 354 Context context = getApplicationContext(); 355 int titleId = CellBroadcastResources.getDialogTitleResource(context, message); 356 357 String title = getText(titleId).toString(); 358 TextView titleTextView = findViewById(R.id.alertTitle); 359 360 if (CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context) 361 .getBoolean(R.bool.show_date_time_title)) { 362 titleTextView.setSingleLine(false); 363 title += "\n" + message.getDateString(context); 364 } 365 366 setTitle(title); 367 titleTextView.setText(title); 368 369 ((TextView) findViewById(R.id.message)).setText(message.getMessageBody()); 370 371 String dismissButtonText = getString(R.string.button_dismiss); 372 373 if (mMessageList.size() > 1) { 374 dismissButtonText += " (1/" + mMessageList.size() + ")"; 375 } 376 377 ((TextView) findViewById(R.id.dismissButton)).setText(dismissButtonText); 378 } 379 380 /** 381 * Called by {@link CellBroadcastAlertService} to add a new alert to the stack. 382 * @param intent The new intent containing one or more {@link CellBroadcastMessage}s. 383 */ 384 @Override onNewIntent(Intent intent)385 protected void onNewIntent(Intent intent) { 386 ArrayList<CellBroadcastMessage> newMessageList = intent.getParcelableArrayListExtra( 387 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); 388 if (newMessageList != null) { 389 if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) { 390 mMessageList = newMessageList; 391 } else { 392 mMessageList.addAll(newMessageList); 393 } 394 Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size()); 395 updateAlertText(getLatestMessage()); 396 // If the new intent was sent from a notification, dismiss it. 397 clearNotification(intent); 398 } else { 399 Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring"); 400 } 401 } 402 403 /** 404 * Try to cancel any notification that may have started this activity. 405 * @param intent Intent containing extras used to identify if notification needs to be cleared 406 */ clearNotification(Intent intent)407 private void clearNotification(Intent intent) { 408 if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) { 409 NotificationManager notificationManager = 410 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 411 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 412 CellBroadcastReceiverApp.clearNewMessageList(); 413 } 414 } 415 416 /** 417 * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio} 418 * service if necessary. 419 */ dismiss()420 void dismiss() { 421 Log.d(TAG, "dismiss"); 422 // Stop playing alert sound/vibration/speech (if started) 423 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 424 425 // Cancel any pending alert reminder 426 CellBroadcastAlertReminder.cancelAlertReminder(); 427 428 // Remove the current alert message from the list. 429 CellBroadcastMessage lastMessage = removeLatestMessage(); 430 if (lastMessage == null) { 431 Log.e(TAG, "dismiss() called with empty message list!"); 432 finish(); 433 return; 434 } 435 436 // Mark the alert as read. 437 final long deliveryTime = lastMessage.getDeliveryTime(); 438 439 // Mark broadcast as read on a background thread. 440 new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver()) 441 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() { 442 @Override 443 public boolean execute(CellBroadcastContentProvider provider) { 444 return provider.markBroadcastRead( 445 Telephony.CellBroadcasts.DELIVERY_TIME, deliveryTime); 446 } 447 }); 448 449 // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert). 450 if (lastMessage.isCmasMessage() && lastMessage.getCmasMessageClass() != 451 SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) { 452 mShowOptOutDialog = true; 453 } 454 455 // If there are older emergency alerts to display, update the alert text and return. 456 CellBroadcastMessage nextMessage = getLatestMessage(); 457 if (nextMessage != null) { 458 updateAlertText(nextMessage); 459 if (CellBroadcastChannelManager.isEmergencyMessage( 460 this, nextMessage)) { 461 mAnimationHandler.startIconAnimation(); 462 } else { 463 mAnimationHandler.stopIconAnimation(); 464 } 465 return; 466 } 467 468 // Remove pending screen-off messages (animation messages are removed in onPause()). 469 mScreenOffHandler.stopScreenOnTimer(); 470 471 // Show opt-in/opt-out dialog when the first CMAS alert is received. 472 if (mShowOptOutDialog) { 473 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 474 if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) { 475 // Clear the flag so the user will only see the opt-out dialog once. 476 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false) 477 .apply(); 478 479 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 480 if (km.inKeyguardRestrictedInputMode()) { 481 Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)"); 482 Intent intent = new Intent(this, CellBroadcastOptOutActivity.class); 483 startActivity(intent); 484 } else { 485 Log.d(TAG, "Showing opt-out dialog in current activity"); 486 CellBroadcastOptOutActivity.showOptOutDialog(this); 487 return; // don't call finish() until user dismisses the dialog 488 } 489 } 490 } 491 NotificationManager notificationManager = 492 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 493 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 494 finish(); 495 } 496 497 @Override dispatchKeyEvent(KeyEvent event)498 public boolean dispatchKeyEvent(KeyEvent event) { 499 CellBroadcastMessage message = getLatestMessage(); 500 if (message != null && !message.isEtwsMessage()) { 501 switch (event.getKeyCode()) { 502 // Volume keys and camera keys mute the alert sound/vibration (except ETWS). 503 case KeyEvent.KEYCODE_VOLUME_UP: 504 case KeyEvent.KEYCODE_VOLUME_DOWN: 505 case KeyEvent.KEYCODE_VOLUME_MUTE: 506 case KeyEvent.KEYCODE_CAMERA: 507 case KeyEvent.KEYCODE_FOCUS: 508 // Stop playing alert sound/vibration/speech (if started) 509 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 510 return true; 511 512 default: 513 break; 514 } 515 } 516 return super.dispatchKeyEvent(event); 517 } 518 519 @Override onBackPressed()520 public void onBackPressed() { 521 // Disable back key 522 } 523 } 524