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.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.app.Activity; 22 import android.app.AlertDialog; 23 import android.app.KeyguardManager; 24 import android.app.NotificationManager; 25 import android.content.ClipData; 26 import android.content.ClipboardManager; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.SharedPreferences; 30 import android.content.res.Configuration; 31 import android.content.res.Resources; 32 import android.graphics.Point; 33 import android.graphics.drawable.Drawable; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.Message; 37 import android.os.PowerManager; 38 import android.preference.PreferenceManager; 39 import android.provider.Telephony; 40 import android.telephony.SmsCbCmasInfo; 41 import android.telephony.SmsCbMessage; 42 import android.telephony.SubscriptionManager; 43 import android.text.Spannable; 44 import android.text.SpannableString; 45 import android.text.format.DateUtils; 46 import android.text.method.LinkMovementMethod; 47 import android.text.util.Linkify; 48 import android.util.Log; 49 import android.view.Display; 50 import android.view.KeyEvent; 51 import android.view.LayoutInflater; 52 import android.view.View; 53 import android.view.ViewGroup; 54 import android.view.Window; 55 import android.view.WindowManager; 56 import android.view.textclassifier.TextClassifier; 57 import android.view.textclassifier.TextLinks; 58 import android.widget.ImageView; 59 import android.widget.TextView; 60 import android.widget.Toast; 61 62 import com.android.internal.annotations.VisibleForTesting; 63 64 import java.lang.annotation.Retention; 65 import java.lang.annotation.RetentionPolicy; 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 import java.util.Collections; 69 import java.util.Comparator; 70 import java.util.concurrent.atomic.AtomicInteger; 71 72 /** 73 * Custom alert dialog with optional flashing warning icon. 74 * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}. 75 */ 76 public class CellBroadcastAlertDialog extends Activity { 77 78 private static final String TAG = "CellBroadcastAlertDialog"; 79 80 /** Intent extra for non-emergency alerts sent when user selects the notification. */ 81 @VisibleForTesting 82 public static final String FROM_NOTIFICATION_EXTRA = "from_notification"; 83 84 // Intent extra to identify if notification was sent while trying to move away from the dialog 85 // without acknowledging the dialog 86 static final String FROM_SAVE_STATE_NOTIFICATION_EXTRA = "from_save_state_notification"; 87 88 /** Not link any text. */ 89 private static final int LINK_METHOD_NONE = 0; 90 91 private static final String LINK_METHOD_NONE_STRING = "none"; 92 93 /** Use {@link android.text.util.Linkify} to generate links. */ 94 private static final int LINK_METHOD_LEGACY_LINKIFY = 1; 95 96 private static final String LINK_METHOD_LEGACY_LINKIFY_STRING = "legacy_linkify"; 97 98 /** 99 * Use the machine learning based {@link TextClassifier} to generate links. Will fallback to 100 * {@link #LINK_METHOD_LEGACY_LINKIFY} if not enabled. 101 */ 102 private static final int LINK_METHOD_SMART_LINKIFY = 2; 103 104 private static final String LINK_METHOD_SMART_LINKIFY_STRING = "smart_linkify"; 105 106 /** 107 * Text link method 108 * @hide 109 */ 110 @Retention(RetentionPolicy.SOURCE) 111 @IntDef(prefix = "LINK_METHOD_", 112 value = {LINK_METHOD_NONE, LINK_METHOD_LEGACY_LINKIFY, 113 LINK_METHOD_SMART_LINKIFY}) 114 private @interface LinkMethod {} 115 116 117 /** List of cell broadcast messages to display (oldest to newest). */ 118 protected ArrayList<SmsCbMessage> mMessageList; 119 120 /** Whether a CMAS alert other than Presidential Alert was displayed. */ 121 private boolean mShowOptOutDialog; 122 123 /** Length of time for the warning icon to be visible. */ 124 private static final int WARNING_ICON_ON_DURATION_MSEC = 800; 125 126 /** Length of time for the warning icon to be off. */ 127 private static final int WARNING_ICON_OFF_DURATION_MSEC = 800; 128 129 /** Length of time to keep the screen turned on. */ 130 private static final int KEEP_SCREEN_ON_DURATION_MSEC = 60000; 131 132 /** Animation handler for the flashing warning icon (emergency alerts only). */ 133 @VisibleForTesting 134 public AnimationHandler mAnimationHandler = new AnimationHandler(); 135 136 /** Handler to add and remove screen on flags for emergency alerts. */ 137 private final ScreenOffHandler mScreenOffHandler = new ScreenOffHandler(); 138 139 // Show the opt-out dialog 140 private AlertDialog mOptOutDialog; 141 142 /** 143 * Animation handler for the flashing warning icon (emergency alerts only). 144 */ 145 @VisibleForTesting 146 public class AnimationHandler extends Handler { 147 /** Latest {@code message.what} value for detecting old messages. */ 148 @VisibleForTesting 149 public final AtomicInteger mCount = new AtomicInteger(); 150 151 /** Warning icon state: visible == true, hidden == false. */ 152 @VisibleForTesting 153 public boolean mWarningIconVisible; 154 155 /** The warning icon Drawable. */ 156 private Drawable mWarningIcon; 157 158 /** The View containing the warning icon. */ 159 private ImageView mWarningIconView; 160 161 /** Package local constructor (called from outer class). */ AnimationHandler()162 AnimationHandler() {} 163 164 /** Start the warning icon animation. */ 165 @VisibleForTesting startIconAnimation(int subId)166 public void startIconAnimation(int subId) { 167 if (!initDrawableAndImageView(subId)) { 168 return; // init failure 169 } 170 mWarningIconVisible = true; 171 mWarningIconView.setVisibility(View.VISIBLE); 172 updateIconState(); 173 queueAnimateMessage(); 174 } 175 176 /** Stop the warning icon animation. */ 177 @VisibleForTesting stopIconAnimation()178 public void stopIconAnimation() { 179 // Increment the counter so the handler will ignore the next message. 180 mCount.incrementAndGet(); 181 if (mWarningIconView != null) { 182 mWarningIconView.setVisibility(View.GONE); 183 } 184 } 185 186 /** Update the visibility of the warning icon. */ updateIconState()187 private void updateIconState() { 188 mWarningIconView.setImageAlpha(mWarningIconVisible ? 255 : 0); 189 mWarningIconView.invalidateDrawable(mWarningIcon); 190 } 191 192 /** Queue a message to animate the warning icon. */ queueAnimateMessage()193 private void queueAnimateMessage() { 194 int msgWhat = mCount.incrementAndGet(); 195 sendEmptyMessageDelayed(msgWhat, mWarningIconVisible ? WARNING_ICON_ON_DURATION_MSEC 196 : WARNING_ICON_OFF_DURATION_MSEC); 197 } 198 199 @Override handleMessage(Message msg)200 public void handleMessage(Message msg) { 201 if (msg.what == mCount.get()) { 202 mWarningIconVisible = !mWarningIconVisible; 203 updateIconState(); 204 queueAnimateMessage(); 205 } 206 } 207 208 /** 209 * Initialize the Drawable and ImageView fields. 210 * 211 * @param subId Subscription index 212 * 213 * @return true if successful; false if any field failed to initialize 214 */ initDrawableAndImageView(int subId)215 private boolean initDrawableAndImageView(int subId) { 216 if (mWarningIcon == null) { 217 try { 218 mWarningIcon = CellBroadcastSettings.getResources(getApplicationContext(), 219 subId).getDrawable(R.drawable.ic_warning_googred); 220 } catch (Resources.NotFoundException e) { 221 Log.e(TAG, "warning icon resource not found", e); 222 return false; 223 } 224 } 225 if (mWarningIconView == null) { 226 mWarningIconView = (ImageView) findViewById(R.id.icon); 227 if (mWarningIconView != null) { 228 mWarningIconView.setImageDrawable(mWarningIcon); 229 } else { 230 Log.e(TAG, "failed to get ImageView for warning icon"); 231 return false; 232 } 233 } 234 return true; 235 } 236 } 237 238 /** 239 * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay, 240 * remove the flag so the screen can turn off to conserve the battery. 241 */ 242 private class ScreenOffHandler extends Handler { 243 /** Latest {@code message.what} value for detecting old messages. */ 244 private final AtomicInteger mCount = new AtomicInteger(); 245 246 /** Package local constructor (called from outer class). */ ScreenOffHandler()247 ScreenOffHandler() {} 248 249 /** Add screen on window flags and queue a delayed message to remove them later. */ startScreenOnTimer()250 void startScreenOnTimer() { 251 addWindowFlags(); 252 int msgWhat = mCount.incrementAndGet(); 253 removeMessages(msgWhat - 1); // Remove previous message, if any. 254 sendEmptyMessageDelayed(msgWhat, KEEP_SCREEN_ON_DURATION_MSEC); 255 Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat); 256 } 257 258 /** Remove the screen on window flags and any queued screen off message. */ stopScreenOnTimer()259 void stopScreenOnTimer() { 260 removeMessages(mCount.get()); 261 clearWindowFlags(); 262 } 263 264 /** Set the screen on window flags. */ addWindowFlags()265 private void addWindowFlags() { 266 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 267 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 268 } 269 270 /** Clear the screen on window flags. */ clearWindowFlags()271 private void clearWindowFlags() { 272 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 273 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 274 } 275 276 @Override handleMessage(Message msg)277 public void handleMessage(Message msg) { 278 int msgWhat = msg.what; 279 if (msgWhat == mCount.get()) { 280 clearWindowFlags(); 281 Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat); 282 } else { 283 Log.e(TAG, "discarding screen off message with id " + msgWhat); 284 } 285 } 286 } 287 288 @Override onCreate(Bundle savedInstanceState)289 protected void onCreate(Bundle savedInstanceState) { 290 super.onCreate(savedInstanceState); 291 292 final Window win = getWindow(); 293 294 // We use a custom title, so remove the standard dialog title bar 295 win.requestFeature(Window.FEATURE_NO_TITLE); 296 297 // Full screen alerts display above the keyguard and when device is locked. 298 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN 299 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 300 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 301 302 // Disable home button when alert dialog is showing if mute_by_physical_button is false. 303 if (!CellBroadcastSettings.getResources(getApplicationContext(), 304 SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) 305 .getBoolean(R.bool.mute_by_physical_button)) { 306 final View decorView = win.getDecorView(); 307 decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); 308 } 309 310 setFinishOnTouchOutside(false); 311 312 // Initialize the view. 313 LayoutInflater inflater = LayoutInflater.from(this); 314 setContentView(inflater.inflate(R.layout.cell_broadcast_alert, null)); 315 316 findViewById(R.id.dismissButton).setOnClickListener(v -> dismiss()); 317 318 // Get message list from saved Bundle or from Intent. 319 if (savedInstanceState != null) { 320 Log.d(TAG, "onCreate getting message list from saved instance state"); 321 mMessageList = savedInstanceState.getParcelableArrayList( 322 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA); 323 } else { 324 Log.d(TAG, "onCreate getting message list from intent"); 325 Intent intent = getIntent(); 326 mMessageList = intent.getParcelableArrayListExtra( 327 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA); 328 329 // If we were started from a notification, dismiss it. 330 clearNotification(intent); 331 } 332 333 if (mMessageList == null || mMessageList.size() == 0) { 334 Log.e(TAG, "onCreate failed as message list is null or empty"); 335 finish(); 336 } else { 337 Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size()); 338 339 // For emergency alerts, keep screen on so the user can read it 340 SmsCbMessage message = getLatestMessage(); 341 342 if (message == null) { 343 Log.e(TAG, "message is null"); 344 finish(); 345 return; 346 } 347 348 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 349 this, message.getSubscriptionId()); 350 if (channelManager.isEmergencyMessage(message)) { 351 Log.d(TAG, "onCreate setting screen on timer for emergency alert for sub " 352 + message.getSubscriptionId()); 353 mScreenOffHandler.startScreenOnTimer(); 354 } 355 356 updateAlertText(message); 357 358 Resources res = CellBroadcastSettings.getResources(getApplicationContext(), 359 message.getSubscriptionId()); 360 if (res.getBoolean(R.bool.enable_text_copy)) { 361 TextView textView = findViewById(R.id.message); 362 if (textView != null) { 363 textView.setOnLongClickListener(v -> copyMessageToClipboard(message, 364 getApplicationContext())); 365 } 366 } 367 } 368 } 369 370 @Override onStart()371 public void onStart() { 372 super.onStart(); 373 getWindow().addSystemFlags( 374 android.view.WindowManager.LayoutParams 375 .SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 376 } 377 378 /** 379 * Start animating warning icon. 380 */ 381 @Override 382 @VisibleForTesting onResume()383 public void onResume() { 384 super.onResume(); 385 SmsCbMessage message = getLatestMessage(); 386 if (message != null) { 387 int subId = message.getSubscriptionId(); 388 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(this, 389 subId); 390 if (channelManager.isEmergencyMessage(message)) { 391 mAnimationHandler.startIconAnimation(subId); 392 } 393 } 394 } 395 396 /** 397 * Stop animating warning icon. 398 */ 399 @Override 400 @VisibleForTesting onPause()401 public void onPause() { 402 Log.d(TAG, "onPause called"); 403 mAnimationHandler.stopIconAnimation(); 404 super.onPause(); 405 } 406 407 @Override onStop()408 protected void onStop() { 409 Log.d(TAG, "onStop called"); 410 // When the activity goes in background eg. clicking Home button, send notification. 411 // Avoid doing this when activity will be recreated because of orientation change or if 412 // screen goes off 413 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 414 if (!(isChangingConfigurations() || getLatestMessage() == null) && pm.isScreenOn()) { 415 CellBroadcastAlertService.addToNotificationBar(getLatestMessage(), mMessageList, 416 getApplicationContext(), true); 417 } 418 // Stop playing alert sound/vibration/speech (if started) 419 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 420 super.onStop(); 421 } 422 423 @Override onWindowFocusChanged(boolean hasFocus)424 public void onWindowFocusChanged(boolean hasFocus) { 425 super.onWindowFocusChanged(hasFocus); 426 427 if (hasFocus) { 428 Configuration config = getResources().getConfiguration(); 429 setPictogramAreaLayout(config.orientation); 430 } 431 } 432 433 @Override onConfigurationChanged(Configuration newConfig)434 public void onConfigurationChanged(Configuration newConfig) { 435 super.onConfigurationChanged(newConfig); 436 setPictogramAreaLayout(newConfig.orientation); 437 } 438 439 /** Returns the currently displayed message. */ getLatestMessage()440 SmsCbMessage getLatestMessage() { 441 int index = mMessageList.size() - 1; 442 if (index >= 0) { 443 return mMessageList.get(index); 444 } else { 445 Log.d(TAG, "getLatestMessage returns null"); 446 return null; 447 } 448 } 449 450 /** Removes and returns the currently displayed message. */ removeLatestMessage()451 private SmsCbMessage removeLatestMessage() { 452 int index = mMessageList.size() - 1; 453 if (index >= 0) { 454 return mMessageList.remove(index); 455 } else { 456 return null; 457 } 458 } 459 460 /** 461 * Save the list of messages so the state can be restored later. 462 * @param outState Bundle in which to place the saved state. 463 */ 464 @Override onSaveInstanceState(Bundle outState)465 protected void onSaveInstanceState(Bundle outState) { 466 super.onSaveInstanceState(outState); 467 outState.putParcelableArrayList( 468 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, mMessageList); 469 } 470 471 /** 472 * Get link method 473 * 474 * @param subId Subscription index 475 * @return The link method 476 */ getLinkMethod(int subId)477 private @LinkMethod int getLinkMethod(int subId) { 478 Resources res = CellBroadcastSettings.getResources(getApplicationContext(), subId); 479 switch (res.getString(R.string.link_method)) { 480 case LINK_METHOD_NONE_STRING: return LINK_METHOD_NONE; 481 case LINK_METHOD_LEGACY_LINKIFY_STRING: return LINK_METHOD_LEGACY_LINKIFY; 482 case LINK_METHOD_SMART_LINKIFY_STRING: return LINK_METHOD_SMART_LINKIFY; 483 } 484 return LINK_METHOD_NONE; 485 } 486 487 /** 488 * Add URL links to the applicable texts. 489 * 490 * @param textView Text view 491 * @param messageText The text string of the message 492 * @param linkMethod Link method 493 */ addLinks(@onNull TextView textView, @NonNull String messageText, @LinkMethod int linkMethod)494 private void addLinks(@NonNull TextView textView, @NonNull String messageText, 495 @LinkMethod int linkMethod) { 496 Spannable text = new SpannableString(messageText); 497 if (linkMethod == LINK_METHOD_LEGACY_LINKIFY) { 498 Linkify.addLinks(text, Linkify.ALL); 499 textView.setMovementMethod(LinkMovementMethod.getInstance()); 500 textView.setText(text); 501 } else if (linkMethod == LINK_METHOD_SMART_LINKIFY) { 502 // Text classification cannot be run in the main thread. 503 new Thread(() -> { 504 final TextClassifier classifier = textView.getTextClassifier(); 505 506 TextClassifier.EntityConfig entityConfig = 507 new TextClassifier.EntityConfig.Builder() 508 .setIncludedTypes(Arrays.asList( 509 TextClassifier.TYPE_URL, 510 TextClassifier.TYPE_EMAIL, 511 TextClassifier.TYPE_PHONE, 512 TextClassifier.TYPE_ADDRESS, 513 TextClassifier.TYPE_FLIGHT_NUMBER)) 514 .setExcludedTypes(Arrays.asList( 515 TextClassifier.TYPE_DATE, 516 TextClassifier.TYPE_DATE_TIME)) 517 .build(); 518 519 TextLinks.Request request = new TextLinks.Request.Builder(text) 520 .setEntityConfig(entityConfig) 521 .build(); 522 // Add links to the spannable text. 523 classifier.generateLinks(request).apply( 524 text, TextLinks.APPLY_STRATEGY_REPLACE, null); 525 526 // UI can be only updated in the main thread. 527 runOnUiThread(() -> { 528 textView.setMovementMethod(LinkMovementMethod.getInstance()); 529 textView.setText(text); 530 }); 531 }).start(); 532 } 533 } 534 535 /** 536 * Update alert text when a new emergency alert arrives. 537 * @param message CB message which is used to update alert text. 538 */ updateAlertText(@onNull SmsCbMessage message)539 private void updateAlertText(@NonNull SmsCbMessage message) { 540 Context context = getApplicationContext(); 541 int titleId = CellBroadcastResources.getDialogTitleResource(context, message); 542 543 String title = getText(titleId).toString(); 544 TextView titleTextView = findViewById(R.id.alertTitle); 545 546 Resources res = CellBroadcastSettings.getResources(context, message.getSubscriptionId()); 547 if (titleTextView != null) { 548 if (res.getBoolean(R.bool.show_date_time_title)) { 549 titleTextView.setSingleLine(false); 550 title += "\n" + DateUtils.formatDateTime(context, message.getReceivedTime(), 551 DateUtils.FORMAT_NO_NOON_MIDNIGHT | DateUtils.FORMAT_SHOW_TIME 552 | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE 553 | DateUtils.FORMAT_CAP_AMPM); 554 } 555 556 setTitle(title); 557 titleTextView.setText(title); 558 } 559 560 TextView textView = findViewById(R.id.message); 561 String messageText = message.getMessageBody(); 562 if (textView != null && messageText != null) { 563 int linkMethod = getLinkMethod(message.getSubscriptionId()); 564 if (linkMethod != LINK_METHOD_NONE) { 565 addLinks(textView, messageText, linkMethod); 566 } else { 567 // Do not add any link to the message text. 568 textView.setText(messageText); 569 } 570 } 571 572 String dismissButtonText = getString(R.string.button_dismiss); 573 574 if (mMessageList.size() > 1) { 575 dismissButtonText += " (1/" + mMessageList.size() + ")"; 576 } 577 578 ((TextView) findViewById(R.id.dismissButton)).setText(dismissButtonText); 579 580 581 setPictogram(context, message); 582 } 583 584 /** 585 * Set pictogram image 586 * @param context 587 * @param message 588 */ setPictogram(Context context, SmsCbMessage message)589 private void setPictogram(Context context, SmsCbMessage message) { 590 int resId = CellBroadcastResources.getDialogPictogramResource(context, message); 591 ImageView image = findViewById(R.id.pictogramImage); 592 if (resId != -1) { 593 image.setImageResource(resId); 594 image.setVisibility(View.VISIBLE); 595 } else { 596 image.setVisibility(View.GONE); 597 } 598 } 599 600 /** 601 * Set pictogram to match orientation 602 * 603 * @param orientation The orientation of the pictogram. 604 */ setPictogramAreaLayout(int orientation)605 private void setPictogramAreaLayout(int orientation) { 606 ImageView image = findViewById(R.id.pictogramImage); 607 if (image.getVisibility() == View.VISIBLE) { 608 ViewGroup.LayoutParams params = image.getLayoutParams(); 609 610 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 611 Display display = getWindowManager().getDefaultDisplay(); 612 Point point = new Point(); 613 display.getSize(point); 614 params.width = (int) (point.x * 0.3); 615 params.height = (int) (point.y * 0.3); 616 } else { 617 params.width = ViewGroup.LayoutParams.WRAP_CONTENT; 618 params.height = ViewGroup.LayoutParams.WRAP_CONTENT; 619 } 620 621 image.setLayoutParams(params); 622 } 623 } 624 625 /** 626 * Called by {@link CellBroadcastAlertService} to add a new alert to the stack. 627 * @param intent The new intent containing one or more {@link SmsCbMessage}. 628 */ 629 @Override 630 @VisibleForTesting onNewIntent(Intent intent)631 public void onNewIntent(Intent intent) { 632 ArrayList<SmsCbMessage> newMessageList = intent.getParcelableArrayListExtra( 633 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA); 634 if (newMessageList != null) { 635 if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) { 636 mMessageList = newMessageList; 637 } else { 638 mMessageList.addAll(newMessageList); 639 if (CellBroadcastSettings.getResources(getApplicationContext(), 640 SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) 641 .getBoolean(R.bool.show_cmas_messages_in_priority_order)) { 642 // Sort message list to show messages in a different order than received by 643 // prioritizing them. Presidential Alert only has top priority. 644 Collections.sort( 645 mMessageList, 646 (Comparator) (o1, o2) -> { 647 boolean isPresidentialAlert1 = 648 ((SmsCbMessage) o1).isCmasMessage() 649 && ((SmsCbMessage) o1).getCmasWarningInfo() 650 .getMessageClass() == SmsCbCmasInfo 651 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; 652 boolean isPresidentialAlert2 = 653 ((SmsCbMessage) o2).isCmasMessage() 654 && ((SmsCbMessage) o2).getCmasWarningInfo() 655 .getMessageClass() == SmsCbCmasInfo 656 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; 657 if (isPresidentialAlert1 ^ isPresidentialAlert2) { 658 return isPresidentialAlert1 ? 1 : -1; 659 } 660 Long time1 = 661 new Long(((SmsCbMessage) o1).getReceivedTime()); 662 Long time2 = 663 new Long(((SmsCbMessage) o2).getReceivedTime()); 664 return time2.compareTo(time1); 665 }); 666 } 667 } 668 Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size()); 669 hideOptOutDialog(); // Hide opt-out dialog when new alert coming 670 updateAlertText(getLatestMessage()); 671 // If the new intent was sent from a notification, dismiss it. 672 clearNotification(intent); 673 } else { 674 Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring"); 675 } 676 } 677 678 /** 679 * Try to cancel any notification that may have started this activity. 680 * @param intent Intent containing extras used to identify if notification needs to be cleared 681 */ clearNotification(Intent intent)682 private void clearNotification(Intent intent) { 683 if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) { 684 NotificationManager notificationManager = 685 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 686 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 687 CellBroadcastReceiverApp.clearNewMessageList(); 688 } 689 } 690 691 /** 692 * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio} 693 * service if necessary. 694 */ 695 @VisibleForTesting dismiss()696 public void dismiss() { 697 Log.d(TAG, "dismiss"); 698 // Stop playing alert sound/vibration/speech (if started) 699 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 700 701 // Cancel any pending alert reminder 702 CellBroadcastAlertReminder.cancelAlertReminder(); 703 704 // Remove the current alert message from the list. 705 SmsCbMessage lastMessage = removeLatestMessage(); 706 if (lastMessage == null) { 707 Log.e(TAG, "dismiss() called with empty message list!"); 708 finish(); 709 return; 710 } 711 712 // Mark the alert as read. 713 final long deliveryTime = lastMessage.getReceivedTime(); 714 715 // Mark broadcast as read on a background thread. 716 new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver()) 717 .execute((CellBroadcastContentProvider.CellBroadcastOperation) provider 718 -> provider.markBroadcastRead(Telephony.CellBroadcasts.DELIVERY_TIME, 719 deliveryTime)); 720 721 // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert). 722 if (lastMessage.isCmasMessage() && lastMessage.getCmasWarningInfo().getMessageClass() 723 != SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) { 724 mShowOptOutDialog = true; 725 } 726 727 // If there are older emergency alerts to display, update the alert text and return. 728 SmsCbMessage nextMessage = getLatestMessage(); 729 if (nextMessage != null) { 730 updateAlertText(nextMessage); 731 int subId = nextMessage.getSubscriptionId(); 732 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 733 getApplicationContext(), subId); 734 if (channelManager.isEmergencyMessage(nextMessage)) { 735 mAnimationHandler.startIconAnimation(subId); 736 } else { 737 mAnimationHandler.stopIconAnimation(); 738 } 739 return; 740 } 741 742 // Remove pending screen-off messages (animation messages are removed in onPause()). 743 mScreenOffHandler.stopScreenOnTimer(); 744 745 // Show opt-in/opt-out dialog when the first CMAS alert is received. 746 if (mShowOptOutDialog) { 747 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 748 if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) { 749 // Clear the flag so the user will only see the opt-out dialog once. 750 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false) 751 .apply(); 752 753 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 754 if (km.inKeyguardRestrictedInputMode()) { 755 Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)"); 756 Intent intent = new Intent(this, CellBroadcastOptOutActivity.class); 757 startActivity(intent); 758 } else { 759 Log.d(TAG, "Showing opt-out dialog in current activity"); 760 mOptOutDialog = CellBroadcastOptOutActivity.showOptOutDialog(this); 761 return; // don't call finish() until user dismisses the dialog 762 } 763 } 764 } 765 NotificationManager notificationManager = 766 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 767 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 768 finish(); 769 } 770 771 @Override onKeyDown(int keyCode, KeyEvent event)772 public boolean onKeyDown(int keyCode, KeyEvent event) { 773 Log.d(TAG, "onKeyDown: " + event); 774 SmsCbMessage message = getLatestMessage(); 775 if (CellBroadcastSettings.getResources(getApplicationContext(), message.getSubscriptionId()) 776 .getBoolean(R.bool.mute_by_physical_button)) { 777 switch (event.getKeyCode()) { 778 // Volume keys and camera keys mute the alert sound/vibration (except ETWS). 779 case KeyEvent.KEYCODE_VOLUME_UP: 780 case KeyEvent.KEYCODE_VOLUME_DOWN: 781 case KeyEvent.KEYCODE_VOLUME_MUTE: 782 case KeyEvent.KEYCODE_CAMERA: 783 case KeyEvent.KEYCODE_FOCUS: 784 // Stop playing alert sound/vibration/speech (if started) 785 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 786 return true; 787 788 default: 789 break; 790 } 791 return super.onKeyDown(keyCode, event); 792 } else { 793 if (event.getKeyCode() == KeyEvent.KEYCODE_POWER) { 794 // TODO: do something to prevent screen off 795 } 796 // Disable all physical keys if mute_by_physical_button is false 797 return true; 798 } 799 } 800 801 @Override onBackPressed()802 public void onBackPressed() { 803 // Disable back key 804 } 805 806 /** 807 * Hide opt-out dialog. 808 * In case of any emergency alert invisible, need to hide the opt-out dialog when 809 * new alert coming. 810 */ hideOptOutDialog()811 private void hideOptOutDialog() { 812 if (mOptOutDialog != null && mOptOutDialog.isShowing()) { 813 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 814 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true) 815 .apply(); 816 mOptOutDialog.dismiss(); 817 } 818 } 819 820 /** 821 * Copy the message to clipboard. 822 * 823 * @param message Cell broadcast message. 824 * 825 * @return {@code true} if success, otherwise {@code false}; 826 */ 827 @VisibleForTesting copyMessageToClipboard(SmsCbMessage message, Context context)828 public static boolean copyMessageToClipboard(SmsCbMessage message, Context context) { 829 ClipboardManager cm = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); 830 if (cm == null) return false; 831 832 cm.setPrimaryClip(ClipData.newPlainText("Alert Message", message.getMessageBody())); 833 834 String msg = CellBroadcastSettings.getResources(context, 835 message.getSubscriptionId()).getString(R.string.message_copied); 836 Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); 837 return true; 838 } 839 } 840