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 static com.android.cellbroadcastreceiver.CellBroadcastReceiver.VDBG; 20 import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRSRC_CBR; 21 import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRTYPE_ICONRESOURCE; 22 import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRTYPE_STATUSBAR; 23 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.app.Activity; 27 import android.app.AlertDialog; 28 import android.app.KeyguardManager; 29 import android.app.NotificationManager; 30 import android.app.PendingIntent; 31 import android.app.RemoteAction; 32 import android.app.StatusBarManager; 33 import android.content.BroadcastReceiver; 34 import android.content.ClipData; 35 import android.content.ClipboardManager; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.IntentFilter; 39 import android.content.SharedPreferences; 40 import android.content.res.Configuration; 41 import android.content.res.Resources; 42 import android.graphics.Color; 43 import android.graphics.Point; 44 import android.graphics.drawable.ColorDrawable; 45 import android.graphics.drawable.Drawable; 46 import android.os.Bundle; 47 import android.os.Handler; 48 import android.os.Message; 49 import android.os.PowerManager; 50 import android.preference.PreferenceManager; 51 import android.provider.Telephony; 52 import android.telephony.SmsCbCmasInfo; 53 import android.telephony.SmsCbMessage; 54 import android.text.Spannable; 55 import android.text.SpannableString; 56 import android.text.TextUtils; 57 import android.text.method.LinkMovementMethod; 58 import android.text.style.ClickableSpan; 59 import android.text.util.Linkify; 60 import android.util.Log; 61 import android.view.Display; 62 import android.view.Gravity; 63 import android.view.KeyEvent; 64 import android.view.LayoutInflater; 65 import android.view.View; 66 import android.view.ViewGroup; 67 import android.view.Window; 68 import android.view.WindowManager; 69 import android.view.textclassifier.TextClassification; 70 import android.view.textclassifier.TextClassification.Request; 71 import android.view.textclassifier.TextClassifier; 72 import android.view.textclassifier.TextLinks; 73 import android.view.textclassifier.TextLinks.TextLink; 74 import android.widget.ImageView; 75 import android.widget.TextView; 76 import android.widget.Toast; 77 78 import com.android.cellbroadcastreceiver.CellBroadcastChannelManager.CellBroadcastChannelRange; 79 import com.android.internal.annotations.VisibleForTesting; 80 81 import java.lang.annotation.Retention; 82 import java.lang.annotation.RetentionPolicy; 83 import java.lang.reflect.Method; 84 import java.text.SimpleDateFormat; 85 import java.util.ArrayList; 86 import java.util.Arrays; 87 import java.util.Collections; 88 import java.util.Comparator; 89 import java.util.concurrent.atomic.AtomicInteger; 90 91 /** 92 * Custom alert dialog with optional flashing warning icon. 93 * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}. 94 */ 95 public class CellBroadcastAlertDialog extends Activity { 96 97 private static final String TAG = "CellBroadcastAlertDialog"; 98 99 /** Intent extra indicate this intent should not dismiss the notification */ 100 @VisibleForTesting 101 public static final String DISMISS_NOTIFICATION_EXTRA = "dismiss_notification"; 102 103 // Intent extra to identify if notification was sent while trying to move away from the dialog 104 // without acknowledging the dialog 105 static final String FROM_SAVE_STATE_NOTIFICATION_EXTRA = "from_save_state_notification"; 106 107 /** Not link any text. */ 108 private static final int LINK_METHOD_NONE = 0; 109 110 private static final String LINK_METHOD_NONE_STRING = "none"; 111 112 /** Use {@link android.text.util.Linkify} to generate links. */ 113 private static final int LINK_METHOD_LEGACY_LINKIFY = 1; 114 115 private static final String LINK_METHOD_LEGACY_LINKIFY_STRING = "legacy_linkify"; 116 117 /** 118 * Use the machine learning based {@link TextClassifier} to generate links. Will fallback to 119 * {@link #LINK_METHOD_LEGACY_LINKIFY} if not enabled. 120 */ 121 private static final int LINK_METHOD_SMART_LINKIFY = 2; 122 123 private static final String LINK_METHOD_SMART_LINKIFY_STRING = "smart_linkify"; 124 125 /** 126 * Use the machine learning based {@link TextClassifier} to generate links but hiding copy 127 * option. Will fallback to 128 * {@link #LINK_METHOD_LEGACY_LINKIFY} if not enabled. 129 */ 130 private static final int LINK_METHOD_SMART_LINKIFY_NO_COPY = 3; 131 132 private static final String LINK_METHOD_SMART_LINKIFY_NO_COPY_STRING = "smart_linkify_no_copy"; 133 134 135 /** 136 * Text link method 137 * @hide 138 */ 139 @Retention(RetentionPolicy.SOURCE) 140 @IntDef(prefix = "LINK_METHOD_", 141 value = {LINK_METHOD_NONE, LINK_METHOD_LEGACY_LINKIFY, 142 LINK_METHOD_SMART_LINKIFY, LINK_METHOD_SMART_LINKIFY_NO_COPY}) 143 private @interface LinkMethod {} 144 145 146 /** List of cell broadcast messages to display (oldest to newest). */ 147 protected ArrayList<SmsCbMessage> mMessageList; 148 149 /** Whether a CMAS alert other than Presidential Alert was displayed. */ 150 private boolean mShowOptOutDialog; 151 152 /** Length of time for the warning icon to be visible. */ 153 private static final int WARNING_ICON_ON_DURATION_MSEC = 800; 154 155 /** Length of time for the warning icon to be off. */ 156 private static final int WARNING_ICON_OFF_DURATION_MSEC = 800; 157 158 /** Default interval for the highlight color of the pulsation. */ 159 private static final int PULSATION_ON_DURATION_MSEC = 1000; 160 /** Default interval for the normal color of the pulsation. */ 161 private static final int PULSATION_OFF_DURATION_MSEC = 1000; 162 /** Max value for the interval of the color change. */ 163 private static final int PULSATION_MAX_ON_OFF_DURATION_MSEC = 120000; 164 /** Default time for the pulsation */ 165 private static final int PULSATION_DURATION_MSEC = 10000; 166 /** Max time for the pulsation */ 167 private static final int PULSATION_MAX_DURATION_MSEC = 86400000; 168 169 /** Length of time to keep the screen turned on. */ 170 private static final int KEEP_SCREEN_ON_DURATION_MSEC = 60000; 171 172 /** Animation handler for the flashing warning icon (emergency alerts only). */ 173 @VisibleForTesting 174 public AnimationHandler mAnimationHandler = new AnimationHandler(); 175 176 /** Handler to add and remove screen on flags for emergency alerts. */ 177 private final ScreenOffHandler mScreenOffHandler = new ScreenOffHandler(); 178 179 /** Pulsation handler for the alert background color. */ 180 @VisibleForTesting 181 public PulsationHandler mPulsationHandler = new PulsationHandler(); 182 183 // Show the opt-out dialog 184 private AlertDialog mOptOutDialog; 185 186 /** BroadcastReceiver for screen off events. When screen was off, remove FLAG_TURN_SCREEN_ON to 187 * start from a clean state. Otherwise, the window flags from the first alert will be 188 * automatically applied to the following alerts handled at onNewIntent. 189 */ 190 private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { 191 @Override 192 public void onReceive(Context context, Intent intent){ 193 Log.d(TAG, "onSreenOff: remove FLAG_TURN_SCREEN_ON flag"); 194 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); 195 } 196 }; 197 198 /** 199 * Animation handler for the flashing warning icon (emergency alerts only). 200 */ 201 @VisibleForTesting 202 public class AnimationHandler extends Handler { 203 /** Latest {@code message.what} value for detecting old messages. */ 204 @VisibleForTesting 205 public final AtomicInteger mCount = new AtomicInteger(); 206 207 /** Warning icon state: visible == true, hidden == false. */ 208 @VisibleForTesting 209 public boolean mWarningIconVisible; 210 211 /** The warning icon Drawable. */ 212 private Drawable mWarningIcon; 213 214 /** The View containing the warning icon. */ 215 private ImageView mWarningIconView; 216 217 /** Package local constructor (called from outer class). */ AnimationHandler()218 AnimationHandler() {} 219 220 /** Start the warning icon animation. */ 221 @VisibleForTesting startIconAnimation(int subId)222 public void startIconAnimation(int subId) { 223 if (!initDrawableAndImageView(subId)) { 224 return; // init failure 225 } 226 mWarningIconVisible = true; 227 mWarningIconView.setVisibility(View.VISIBLE); 228 updateIconState(); 229 queueAnimateMessage(); 230 } 231 232 /** Stop the warning icon animation. */ 233 @VisibleForTesting stopIconAnimation()234 public void stopIconAnimation() { 235 // Increment the counter so the handler will ignore the next message. 236 mCount.incrementAndGet(); 237 } 238 239 /** Update the visibility of the warning icon. */ updateIconState()240 private void updateIconState() { 241 mWarningIconView.setImageAlpha(mWarningIconVisible ? 255 : 0); 242 mWarningIconView.invalidateDrawable(mWarningIcon); 243 } 244 245 /** Queue a message to animate the warning icon. */ queueAnimateMessage()246 private void queueAnimateMessage() { 247 int msgWhat = mCount.incrementAndGet(); 248 sendEmptyMessageDelayed(msgWhat, mWarningIconVisible ? WARNING_ICON_ON_DURATION_MSEC 249 : WARNING_ICON_OFF_DURATION_MSEC); 250 } 251 252 @Override handleMessage(Message msg)253 public void handleMessage(Message msg) { 254 if (msg.what == mCount.get()) { 255 mWarningIconVisible = !mWarningIconVisible; 256 updateIconState(); 257 queueAnimateMessage(); 258 } 259 } 260 261 /** 262 * Initialize the Drawable and ImageView fields. 263 * 264 * @param subId Subscription index 265 * 266 * @return true if successful; false if any field failed to initialize 267 */ initDrawableAndImageView(int subId)268 private boolean initDrawableAndImageView(int subId) { 269 if (mWarningIcon == null) { 270 try { 271 mWarningIcon = CellBroadcastSettings.getResourcesByOperator( 272 getApplicationContext(), subId, 273 CellBroadcastReceiver 274 .getRoamingOperatorSupported(getApplicationContext())) 275 .getDrawable(R.drawable.ic_warning_googred); 276 } catch (Resources.NotFoundException e) { 277 CellBroadcastReceiverMetrics.getInstance().logModuleError( 278 ERRSRC_CBR, ERRTYPE_ICONRESOURCE); 279 Log.e(TAG, "warning icon resource not found", e); 280 return false; 281 } 282 } 283 if (mWarningIconView == null) { 284 mWarningIconView = (ImageView) findViewById(R.id.icon); 285 if (mWarningIconView != null) { 286 mWarningIconView.setImageDrawable(mWarningIcon); 287 } else { 288 Log.e(TAG, "failed to get ImageView for warning icon"); 289 return false; 290 } 291 } 292 return true; 293 } 294 } 295 296 /** 297 * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay, 298 * remove the flag so the screen can turn off to conserve the battery. 299 */ 300 private class ScreenOffHandler extends Handler { 301 /** Latest {@code message.what} value for detecting old messages. */ 302 private final AtomicInteger mCount = new AtomicInteger(); 303 304 /** Package local constructor (called from outer class). */ ScreenOffHandler()305 ScreenOffHandler() {} 306 307 /** Add screen on window flags and queue a delayed message to remove them later. */ startScreenOnTimer(@onNull SmsCbMessage message)308 void startScreenOnTimer(@NonNull SmsCbMessage message) { 309 // if screenOnDuration in milliseconds. if set to 0, do not turn screen on. 310 int screenOnDuration = KEEP_SCREEN_ON_DURATION_MSEC; 311 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 312 getApplicationContext(), message.getSubscriptionId()); 313 CellBroadcastChannelRange range = channelManager 314 .getCellBroadcastChannelRangeFromMessage(message); 315 if (range!= null) { 316 screenOnDuration = range.mScreenOnDuration; 317 } 318 if (screenOnDuration == 0) { 319 Log.d(TAG, "screenOnDuration set to 0, do not turn screen on"); 320 return; 321 } 322 addWindowFlags(); 323 int msgWhat = mCount.incrementAndGet(); 324 removeMessages(msgWhat - 1); // Remove previous message, if any. 325 sendEmptyMessageDelayed(msgWhat, screenOnDuration); 326 Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat); 327 } 328 329 /** Remove the screen on window flags and any queued screen off message. */ stopScreenOnTimer()330 void stopScreenOnTimer() { 331 removeMessages(mCount.get()); 332 clearWindowFlags(); 333 } 334 335 /** Set the screen on window flags. */ addWindowFlags()336 private void addWindowFlags() { 337 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 338 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 339 } 340 341 /** 342 * Clear the keep screen on window flags in order for powersaving but keep TURN_ON_SCREEN_ON 343 * to make sure next wake up still turn screen on without unintended onStop triggered at 344 * the beginning. 345 */ clearWindowFlags()346 private void clearWindowFlags() { 347 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 348 } 349 350 @Override handleMessage(Message msg)351 public void handleMessage(Message msg) { 352 int msgWhat = msg.what; 353 if (msgWhat == mCount.get()) { 354 clearWindowFlags(); 355 Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat); 356 } else { 357 Log.e(TAG, "discarding screen off message with id " + msgWhat); 358 } 359 } 360 } 361 362 /** 363 * Pulsation handler for the alert window background color. 364 */ 365 @VisibleForTesting 366 public static class PulsationHandler extends Handler { 367 /** Latest {@code message.what} value for detecting old messages. */ 368 @VisibleForTesting 369 public final AtomicInteger mCount = new AtomicInteger(); 370 371 @VisibleForTesting 372 public int mBackgroundColor = Color.TRANSPARENT; 373 @VisibleForTesting 374 public int mHighlightColor = Color.TRANSPARENT; 375 @VisibleForTesting 376 public int mOnInterval; 377 @VisibleForTesting 378 public int mOffInterval; 379 @VisibleForTesting 380 public int mDuration; 381 @VisibleForTesting 382 public boolean mIsPulsationOn; 383 @VisibleForTesting 384 public View mLayout; 385 386 /** Package local constructor (called from outer class). */ PulsationHandler()387 PulsationHandler() { 388 } 389 390 /** Start the pulsation. */ 391 @VisibleForTesting start(View layout, int[] pattern)392 public void start(View layout, int[] pattern) { 393 if (layout == null || pattern == null || pattern.length == 0) { 394 Log.d(TAG, layout == null ? "layout is null" : "no pulsation pattern"); 395 return; 396 } 397 398 post(() -> { 399 mLayout = layout; 400 Drawable bg = mLayout.getBackground(); 401 if (bg instanceof ColorDrawable) { 402 mBackgroundColor = ((ColorDrawable) bg).getColor(); 403 } 404 405 mHighlightColor = pattern[0]; 406 mDuration = PULSATION_DURATION_MSEC; 407 if (pattern.length > 1) { 408 if (pattern[1] < 0 || pattern[1] > PULSATION_MAX_DURATION_MSEC) { 409 Log.wtf(TAG, "Invalid pulsation duration: " + pattern[1]); 410 } else { 411 mDuration = pattern[1]; 412 } 413 } 414 415 mOnInterval = PULSATION_ON_DURATION_MSEC; 416 if (pattern.length > 2) { 417 if (pattern[2] < 0 || pattern[2] > PULSATION_MAX_ON_OFF_DURATION_MSEC) { 418 Log.wtf(TAG, "Invalid pulsation on interval: " + pattern[2]); 419 } else { 420 mOnInterval = pattern[2]; 421 } 422 } 423 424 mOffInterval = PULSATION_OFF_DURATION_MSEC; 425 if (pattern.length > 3) { 426 if (pattern[3] < 0 || pattern[3] > PULSATION_MAX_ON_OFF_DURATION_MSEC) { 427 Log.wtf(TAG, "Invalid pulsation off interval: " + pattern[3]); 428 } else { 429 mOffInterval = pattern[3]; 430 } 431 } 432 433 if (VDBG) { 434 Log.d(TAG, "start pulsation, highlight color=" + mHighlightColor 435 + ", background color=" + mBackgroundColor 436 + ", duration=" + mDuration 437 + ", on=" + mOnInterval + ", off=" + mOffInterval); 438 } 439 440 mCount.set(0); 441 queuePulsationMessage(); 442 postDelayed(() -> onPulsationStopped(), mDuration); 443 }); 444 } 445 446 /** Stop the pulsation. */ 447 @VisibleForTesting stop()448 public void stop() { 449 post(() -> onPulsationStopped()); 450 } 451 onPulsationStopped()452 private void onPulsationStopped() { 453 // Increment the counter so the handler will ignore the next message. 454 mCount.incrementAndGet(); 455 if (mLayout != null) { 456 mLayout.setBackgroundColor(mBackgroundColor); 457 } 458 mLayout = null; 459 mIsPulsationOn = false; 460 if (VDBG) { 461 Log.d(TAG, "pulsation stopped"); 462 } 463 } 464 465 /** Queue a message to pulsate the background color of the alert. */ queuePulsationMessage()466 private void queuePulsationMessage() { 467 int msgWhat = mCount.incrementAndGet(); 468 sendEmptyMessageDelayed(msgWhat, mIsPulsationOn ? mOnInterval : mOffInterval); 469 } 470 471 @Override handleMessage(Message msg)472 public void handleMessage(Message msg) { 473 if (mLayout == null) { 474 return; 475 } 476 477 if (msg.what == mCount.get()) { 478 mIsPulsationOn = !mIsPulsationOn; 479 mLayout.setBackgroundColor(mIsPulsationOn ? mHighlightColor 480 : mBackgroundColor); 481 queuePulsationMessage(); 482 } 483 } 484 } 485 486 Comparator<SmsCbMessage> mPriorityBasedComparator = (Comparator) (o1, o2) -> { 487 boolean isPresidentialAlert1 = 488 ((SmsCbMessage) o1).isCmasMessage() 489 && ((SmsCbMessage) o1).getCmasWarningInfo() 490 .getMessageClass() == SmsCbCmasInfo 491 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; 492 boolean isPresidentialAlert2 = 493 ((SmsCbMessage) o2).isCmasMessage() 494 && ((SmsCbMessage) o2).getCmasWarningInfo() 495 .getMessageClass() == SmsCbCmasInfo 496 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; 497 if (isPresidentialAlert1 ^ isPresidentialAlert2) { 498 return isPresidentialAlert1 ? 1 : -1; 499 } 500 Long time1 = new Long(((SmsCbMessage) o1).getReceivedTime()); 501 Long time2 = new Long(((SmsCbMessage) o2).getReceivedTime()); 502 return time2.compareTo(time1); 503 }; 504 505 @Override onCreate(Bundle savedInstanceState)506 protected void onCreate(Bundle savedInstanceState) { 507 super.onCreate(savedInstanceState); 508 // if this is only to dismiss any pending alert dialog 509 if (getIntent().getBooleanExtra(CellBroadcastAlertService.DISMISS_DIALOG, false)) { 510 dismissAllFromNotification(getIntent()); 511 return; 512 } 513 514 final Window win = getWindow(); 515 516 // We use a custom title, so remove the standard dialog title bar 517 win.requestFeature(Window.FEATURE_NO_TITLE); 518 519 // Full screen alerts display above the keyguard and when device is locked. 520 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN 521 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 522 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 523 524 // Disable home button when alert dialog is showing if mute_by_physical_button is false. 525 if (!CellBroadcastSettings.getResourcesForDefaultSubId(getApplicationContext()) 526 .getBoolean(R.bool.mute_by_physical_button) && !CellBroadcastSettings 527 .getResourcesForDefaultSubId(getApplicationContext()) 528 .getBoolean(R.bool.disable_status_bar)) { 529 final View decorView = win.getDecorView(); 530 decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); 531 } 532 533 // Initialize the view. 534 LayoutInflater inflater = LayoutInflater.from(this); 535 setContentView(inflater.inflate(R.layout.cell_broadcast_alert, null)); 536 537 findViewById(R.id.dismissButton).setOnClickListener(v -> dismiss()); 538 539 // Get message list from saved Bundle or from Intent. 540 if (savedInstanceState != null) { 541 Log.d(TAG, "onCreate getting message list from saved instance state"); 542 mMessageList = savedInstanceState.getParcelableArrayList( 543 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA); 544 } else { 545 Log.d(TAG, "onCreate getting message list from intent"); 546 Intent intent = getIntent(); 547 mMessageList = intent.getParcelableArrayListExtra( 548 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA); 549 550 // If we were started from a notification, dismiss it. 551 clearNotification(intent); 552 } 553 554 registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); 555 556 if (mMessageList == null || mMessageList.size() == 0) { 557 Log.e(TAG, "onCreate failed as message list is null or empty"); 558 finish(); 559 } else { 560 Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size()); 561 562 // For emergency alerts, keep screen on so the user can read it 563 SmsCbMessage message = getLatestMessage(); 564 565 if (message == null) { 566 Log.e(TAG, "message is null"); 567 finish(); 568 return; 569 } 570 571 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 572 this, message.getSubscriptionId()); 573 if (channelManager.isEmergencyMessage(message)) { 574 Log.d(TAG, "onCreate setting screen on timer for emergency alert for sub " 575 + message.getSubscriptionId()); 576 mScreenOffHandler.startScreenOnTimer(message); 577 } 578 579 setFinishAlertOnTouchOutside(); 580 581 updateAlertText(message); 582 583 Resources res = CellBroadcastSettings.getResourcesByOperator(getApplicationContext(), 584 message.getSubscriptionId(), 585 CellBroadcastReceiver.getRoamingOperatorSupported(getApplicationContext())); 586 if (res.getBoolean(R.bool.enable_text_copy)) { 587 TextView textView = findViewById(R.id.message); 588 if (textView != null) { 589 textView.setOnLongClickListener(v -> copyMessageToClipboard(message, 590 getApplicationContext())); 591 } 592 } 593 594 if (res.getBoolean(R.bool.disable_capture_alert_dialog)) { 595 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); 596 } 597 startPulsatingAsNeeded(channelManager 598 .getCellBroadcastChannelRangeFromMessage(message)); 599 } 600 } 601 602 @Override onStart()603 public void onStart() { 604 super.onStart(); 605 getWindow().addSystemFlags( 606 android.view.WindowManager.LayoutParams 607 .SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 608 } 609 610 /** 611 * Start animating warning icon. 612 */ 613 @Override 614 @VisibleForTesting onResume()615 public void onResume() { 616 super.onResume(); 617 setWindowBottom(); 618 setMaxHeightScrollView(); 619 SmsCbMessage message = getLatestMessage(); 620 if (message != null) { 621 int subId = message.getSubscriptionId(); 622 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(this, 623 subId); 624 CellBroadcastChannelRange range = channelManager 625 .getCellBroadcastChannelRangeFromMessage(message); 626 if (channelManager.isEmergencyMessage(message) 627 && (range!= null && range.mDisplayIcon)) { 628 mAnimationHandler.startIconAnimation(subId); 629 } 630 } 631 // Some LATAM carriers mandate to disable navigation bars, quick settings etc when alert 632 // dialog is showing. This is to make sure users to ack the alert before switching to 633 // other activities. 634 setStatusBarDisabledIfNeeded(true); 635 } 636 637 /** 638 * Stop animating warning icon. 639 */ 640 @Override 641 @VisibleForTesting onPause()642 public void onPause() { 643 Log.d(TAG, "onPause called"); 644 mAnimationHandler.stopIconAnimation(); 645 setStatusBarDisabledIfNeeded(false); 646 super.onPause(); 647 } 648 649 @Override onUserLeaveHint()650 protected void onUserLeaveHint() { 651 Log.d(TAG, "onUserLeaveHint called"); 652 // When the activity goes in background (eg. clicking Home button, dismissed by outside 653 // touch if enabled), send notification. 654 // Avoid doing this when activity will be recreated because of orientation change or if 655 // screen goes off 656 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 657 ArrayList<SmsCbMessage> messageList = getNewMessageListIfNeeded(mMessageList, 658 CellBroadcastReceiverApp.getNewMessageList()); 659 SmsCbMessage latestMessage = (messageList == null || (messageList.size() < 1)) ? null 660 : messageList.get(messageList.size() - 1); 661 662 if (!(isChangingConfigurations() || latestMessage == null) && pm.isScreenOn()) { 663 Log.d(TAG, "call addToNotificationBar when activity goes in background"); 664 CellBroadcastAlertService.addToNotificationBar(latestMessage, messageList, 665 getApplicationContext(), true, true, false); 666 } 667 super.onUserLeaveHint(); 668 } 669 670 @Override onWindowFocusChanged(boolean hasFocus)671 public void onWindowFocusChanged(boolean hasFocus) { 672 super.onWindowFocusChanged(hasFocus); 673 674 if (hasFocus) { 675 Configuration config = getResources().getConfiguration(); 676 setPictogramAreaLayout(config.orientation); 677 } 678 } 679 680 @Override onConfigurationChanged(Configuration newConfig)681 public void onConfigurationChanged(Configuration newConfig) { 682 super.onConfigurationChanged(newConfig); 683 setPictogramAreaLayout(newConfig.orientation); 684 } 685 setWindowBottom()686 private void setWindowBottom() { 687 // some OEMs require that the alert window is moved to the bottom of the screen to avoid 688 // blocking other screen content 689 if (getResources().getBoolean(R.bool.alert_dialog_bottom)) { 690 Window window = getWindow(); 691 WindowManager.LayoutParams params = window.getAttributes(); 692 params.height = WindowManager.LayoutParams.WRAP_CONTENT; 693 params.gravity = params.gravity | Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 694 params.verticalMargin = 0; 695 window.setAttributes(params); 696 } 697 } 698 699 /** Returns the currently displayed message. */ getLatestMessage()700 SmsCbMessage getLatestMessage() { 701 int index = mMessageList.size() - 1; 702 if (index >= 0) { 703 return mMessageList.get(index); 704 } else { 705 Log.d(TAG, "getLatestMessage returns null"); 706 return null; 707 } 708 } 709 710 /** Removes and returns the currently displayed message. */ removeLatestMessage()711 private SmsCbMessage removeLatestMessage() { 712 int index = mMessageList.size() - 1; 713 if (index >= 0) { 714 return mMessageList.remove(index); 715 } else { 716 return null; 717 } 718 } 719 720 /** 721 * Save the list of messages so the state can be restored later. 722 * @param outState Bundle in which to place the saved state. 723 */ 724 @Override onSaveInstanceState(Bundle outState)725 protected void onSaveInstanceState(Bundle outState) { 726 super.onSaveInstanceState(outState); 727 outState.putParcelableArrayList( 728 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, mMessageList); 729 } 730 731 /** 732 * Get link method 733 * 734 * @param subId Subscription index 735 * @return The link method 736 */ getLinkMethod(int subId)737 private @LinkMethod int getLinkMethod(int subId) { 738 Resources res = CellBroadcastSettings.getResourcesByOperator(getApplicationContext(), 739 subId, CellBroadcastReceiver.getRoamingOperatorSupported(getApplicationContext())); 740 switch (res.getString(R.string.link_method)) { 741 case LINK_METHOD_NONE_STRING: return LINK_METHOD_NONE; 742 case LINK_METHOD_LEGACY_LINKIFY_STRING: return LINK_METHOD_LEGACY_LINKIFY; 743 case LINK_METHOD_SMART_LINKIFY_STRING: return LINK_METHOD_SMART_LINKIFY; 744 case LINK_METHOD_SMART_LINKIFY_NO_COPY_STRING: return LINK_METHOD_SMART_LINKIFY_NO_COPY; 745 } 746 return LINK_METHOD_NONE; 747 } 748 749 /** 750 * Add URL links to the applicable texts. 751 * 752 * @param textView Text view 753 * @param messageText The text string of the message 754 * @param linkMethod Link method 755 */ addLinks(@onNull TextView textView, @NonNull String messageText, @LinkMethod int linkMethod)756 private void addLinks(@NonNull TextView textView, @NonNull String messageText, 757 @LinkMethod int linkMethod) { 758 if (linkMethod == LINK_METHOD_LEGACY_LINKIFY) { 759 Spannable text = new SpannableString(messageText); 760 Linkify.addLinks(text, Linkify.ALL); 761 textView.setMovementMethod(LinkMovementMethod.getInstance()); 762 textView.setText(text); 763 } else if (linkMethod == LINK_METHOD_SMART_LINKIFY 764 || linkMethod == LINK_METHOD_SMART_LINKIFY_NO_COPY) { 765 // Text classification cannot be run in the main thread. 766 new Thread(() -> { 767 final TextClassifier classifier = textView.getTextClassifier(); 768 769 TextClassifier.EntityConfig entityConfig = 770 new TextClassifier.EntityConfig.Builder() 771 .setIncludedTypes(Arrays.asList( 772 TextClassifier.TYPE_URL, 773 TextClassifier.TYPE_EMAIL, 774 TextClassifier.TYPE_PHONE, 775 TextClassifier.TYPE_ADDRESS, 776 TextClassifier.TYPE_FLIGHT_NUMBER)) 777 .setExcludedTypes(Arrays.asList( 778 TextClassifier.TYPE_DATE, 779 TextClassifier.TYPE_DATE_TIME)) 780 .build(); 781 782 TextLinks.Request request = new TextLinks.Request.Builder(messageText) 783 .setEntityConfig(entityConfig) 784 .build(); 785 Spannable text; 786 if (linkMethod == LINK_METHOD_SMART_LINKIFY) { 787 text = new SpannableString(messageText); 788 // Add links to the spannable text. 789 classifier.generateLinks(request).apply( 790 text, TextLinks.APPLY_STRATEGY_REPLACE, null); 791 } else { 792 TextLinks textLinks = classifier.generateLinks(request); 793 // Add links to the spannable text. 794 text = applyTextLinksToSpannable(messageText, textLinks, classifier); 795 } 796 // UI can be only updated in the main thread. 797 runOnUiThread(() -> { 798 textView.setMovementMethod(LinkMovementMethod.getInstance()); 799 textView.setText(text); 800 }); 801 }).start(); 802 } 803 } 804 applyTextLinksToSpannable(String text, TextLinks textLinks, TextClassifier textClassifier)805 private Spannable applyTextLinksToSpannable(String text, TextLinks textLinks, 806 TextClassifier textClassifier) { 807 Spannable result = new SpannableString(text); 808 for (TextLink link : textLinks.getLinks()) { 809 TextClassification textClassification = textClassifier.classifyText( 810 new Request.Builder( 811 text, 812 link.getStart(), 813 link.getEnd()) 814 .build()); 815 if (textClassification.getActions().isEmpty()) { 816 continue; 817 } 818 RemoteAction remoteAction = textClassification.getActions().get(0); 819 result.setSpan(new RemoteActionSpan(remoteAction), link.getStart(), link.getEnd(), 820 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 821 } 822 return result; 823 } 824 825 private static class RemoteActionSpan extends ClickableSpan { 826 private final RemoteAction mRemoteAction; RemoteActionSpan(RemoteAction remoteAction)827 private RemoteActionSpan(RemoteAction remoteAction) { 828 mRemoteAction = remoteAction; 829 } 830 @Override onClick(@onNull View view)831 public void onClick(@NonNull View view) { 832 try { 833 mRemoteAction.getActionIntent().send(); 834 } catch (PendingIntent.CanceledException e) { 835 Log.e(TAG, "Failed to start the pendingintent."); 836 } 837 } 838 } 839 840 /** 841 * Update alert text when a new emergency alert arrives. 842 * @param message CB message which is used to update alert text. 843 */ updateAlertText(@onNull SmsCbMessage message)844 private void updateAlertText(@NonNull SmsCbMessage message) { 845 if (message == null) { 846 return; 847 } 848 Context context = getApplicationContext(); 849 int titleId = CellBroadcastResources.getDialogTitleResource(context, message); 850 851 Resources res = CellBroadcastSettings.getResourcesByOperator(context, 852 message.getSubscriptionId(), 853 CellBroadcastReceiver.getRoamingOperatorSupported(context)); 854 855 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 856 this, message.getSubscriptionId()); 857 CellBroadcastChannelRange range = channelManager 858 .getCellBroadcastChannelRangeFromMessage(message); 859 String languageCode; 860 if (range != null && !TextUtils.isEmpty(range.mLanguageCode)) { 861 languageCode = range.mLanguageCode; 862 } else { 863 languageCode = message.getLanguageCode(); 864 } 865 866 if (res.getBoolean(R.bool.show_alert_title)) { 867 String title = CellBroadcastResources.overrideTranslation(context, titleId, res, 868 languageCode); 869 TextView titleTextView = findViewById(R.id.alertTitle); 870 871 if (titleTextView != null) { 872 String timeFormat = res.getString(R.string.date_time_format); 873 if (!TextUtils.isEmpty(timeFormat)) { 874 titleTextView.setSingleLine(false); 875 title += "\n" + new SimpleDateFormat(timeFormat).format( 876 message.getReceivedTime()); 877 } 878 setTitle(title); 879 titleTextView.setText(title); 880 } 881 } else { 882 TextView titleTextView = findViewById(R.id.alertTitle); 883 setTitle(""); 884 titleTextView.setText(""); 885 } 886 887 TextView textView = findViewById(R.id.message); 888 String messageText = message.getMessageBody(); 889 if (textView != null && messageText != null) { 890 int linkMethod = getLinkMethod(message.getSubscriptionId()); 891 if (linkMethod != LINK_METHOD_NONE) { 892 addLinks(textView, messageText, linkMethod); 893 } else { 894 // Do not add any link to the message text. 895 textView.setText(messageText); 896 } 897 } 898 899 String dismissButtonText = getString(R.string.button_dismiss); 900 901 if (mMessageList.size() > 1) { 902 dismissButtonText += " (1/" + mMessageList.size() + ")"; 903 } 904 905 ((TextView) findViewById(R.id.dismissButton)).setText(dismissButtonText); 906 907 setPictogram(context, message); 908 909 if (this.hasWindowFocus()) { 910 Configuration config = res.getConfiguration(); 911 setPictogramAreaLayout(config.orientation); 912 } 913 } 914 915 /** 916 * Set pictogram image 917 * @param context 918 * @param message 919 */ setPictogram(Context context, SmsCbMessage message)920 private void setPictogram(Context context, SmsCbMessage message) { 921 int resId = CellBroadcastResources.getDialogPictogramResource(context, message); 922 ImageView image = findViewById(R.id.pictogramImage); 923 // not all layouts may have a pictogram image, e.g. watch 924 if (image == null) { 925 return; 926 } 927 if (resId != -1) { 928 image.setImageResource(resId); 929 image.setVisibility(View.VISIBLE); 930 } else { 931 image.setVisibility(View.GONE); 932 } 933 } 934 935 /** 936 * Set pictogram to match orientation 937 * 938 * @param orientation The orientation of the pictogram. 939 */ setPictogramAreaLayout(int orientation)940 private void setPictogramAreaLayout(int orientation) { 941 ImageView image = findViewById(R.id.pictogramImage); 942 // not all layouts may have a pictogram image, e.g. watch 943 if (image == null) { 944 return; 945 } 946 if (image.getVisibility() == View.VISIBLE) { 947 ViewGroup.LayoutParams params = image.getLayoutParams(); 948 949 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 950 Display display = getWindowManager().getDefaultDisplay(); 951 Point point = new Point(); 952 display.getSize(point); 953 params.width = (int) (point.x * 0.3); 954 params.height = (int) (point.y * 0.3); 955 } else { 956 params.width = ViewGroup.LayoutParams.WRAP_CONTENT; 957 params.height = ViewGroup.LayoutParams.WRAP_CONTENT; 958 } 959 960 image.setLayoutParams(params); 961 } 962 } 963 setMaxHeightScrollView()964 private void setMaxHeightScrollView() { 965 int contentPanelMaxHeight = getResources().getDimensionPixelSize( 966 R.dimen.alert_dialog_maxheight_content_panel); 967 if (contentPanelMaxHeight > 0) { 968 CustomHeightScrollView scrollView = (CustomHeightScrollView) findViewById( 969 R.id.scrollView); 970 if (scrollView != null) { 971 scrollView.setMaximumHeight(contentPanelMaxHeight); 972 } 973 } 974 } 975 startPulsatingAsNeeded(CellBroadcastChannelRange range)976 private void startPulsatingAsNeeded(CellBroadcastChannelRange range) { 977 mPulsationHandler.stop(); 978 if (VDBG) { 979 Log.d(TAG, "start pulsation as needed for range:" + range); 980 } 981 if (range != null) { 982 mPulsationHandler.start(findViewById(R.id.parentPanel), range.mPulsationPattern); 983 } 984 } 985 986 /** 987 * Called by {@link CellBroadcastAlertService} to add a new alert to the stack. 988 * @param intent The new intent containing one or more {@link SmsCbMessage}. 989 */ 990 @Override 991 @VisibleForTesting onNewIntent(Intent intent)992 public void onNewIntent(Intent intent) { 993 if (intent.getBooleanExtra(CellBroadcastAlertService.DISMISS_DIALOG, false)) { 994 dismissAllFromNotification(intent); 995 return; 996 } 997 ArrayList<SmsCbMessage> newMessageList = intent.getParcelableArrayListExtra( 998 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA); 999 if (newMessageList != null) { 1000 if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) { 1001 mMessageList = newMessageList; 1002 } else { 1003 // remove the duplicate messages 1004 for (SmsCbMessage message : newMessageList) { 1005 mMessageList.removeIf( 1006 msg -> msg.getReceivedTime() == message.getReceivedTime()); 1007 } 1008 mMessageList.addAll(newMessageList); 1009 if (CellBroadcastSettings.getResourcesForDefaultSubId(getApplicationContext()) 1010 .getBoolean(R.bool.show_cmas_messages_in_priority_order)) { 1011 // Sort message list to show messages in a different order than received by 1012 // prioritizing them. Presidential Alert only has top priority. 1013 Collections.sort(mMessageList, mPriorityBasedComparator); 1014 } 1015 } 1016 Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size()); 1017 1018 // For emergency alerts, keep screen on so the user can read it 1019 SmsCbMessage message = getLatestMessage(); 1020 if (message != null) { 1021 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 1022 this, message.getSubscriptionId()); 1023 if (channelManager.isEmergencyMessage(message)) { 1024 Log.d(TAG, "onCreate setting screen on timer for emergency alert for sub " 1025 + message.getSubscriptionId()); 1026 mScreenOffHandler.startScreenOnTimer(message); 1027 } 1028 startPulsatingAsNeeded(channelManager 1029 .getCellBroadcastChannelRangeFromMessage(message)); 1030 } 1031 1032 hideOptOutDialog(); // Hide opt-out dialog when new alert coming 1033 setFinishAlertOnTouchOutside(); 1034 updateAlertText(getLatestMessage()); 1035 // If the new intent was sent from a notification, dismiss it. 1036 clearNotification(intent); 1037 } else { 1038 Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring"); 1039 } 1040 } 1041 1042 /** 1043 * Try to cancel any notification that may have started this activity. 1044 * @param intent Intent containing extras used to identify if notification needs to be cleared 1045 */ clearNotification(Intent intent)1046 private void clearNotification(Intent intent) { 1047 if (intent.getBooleanExtra(DISMISS_NOTIFICATION_EXTRA, false)) { 1048 NotificationManager notificationManager = 1049 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 1050 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 1051 1052 // Clear new message list when user swipe the notification 1053 // except dialog and notification are visible at the same time. 1054 if (intent.getBooleanExtra(CellBroadcastAlertService.DISMISS_DIALOG, false)) { 1055 CellBroadcastReceiverApp.clearNewMessageList(); 1056 } 1057 } 1058 } 1059 1060 /** 1061 * This will be called when users swipe away the notification, this will 1062 * 1. dismiss all foreground dialog, stop animating warning icon and stop the 1063 * {@link CellBroadcastAlertAudio} service. 1064 * 2. Does not mark message read. 1065 */ dismissAllFromNotification(Intent intent)1066 public void dismissAllFromNotification(Intent intent) { 1067 Log.d(TAG, "dismissAllFromNotification"); 1068 // Stop playing alert sound/vibration/speech (if started) 1069 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 1070 // Cancel any pending alert reminder 1071 CellBroadcastAlertReminder.cancelAlertReminder(); 1072 // Remove the all current showing alert message from the list. 1073 if (mMessageList != null) { 1074 mMessageList.clear(); 1075 } 1076 // clear notifications. 1077 clearNotification(intent); 1078 // Remove pending screen-off messages (animation messages are removed in onPause()). 1079 mScreenOffHandler.stopScreenOnTimer(); 1080 finish(); 1081 } 1082 1083 /** 1084 * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio} 1085 * service if necessary. 1086 */ 1087 @VisibleForTesting dismiss()1088 public void dismiss() { 1089 Log.d(TAG, "dismiss"); 1090 // Stop playing alert sound/vibration/speech (if started) 1091 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 1092 1093 mPulsationHandler.stop(); 1094 1095 // Cancel any pending alert reminder 1096 CellBroadcastAlertReminder.cancelAlertReminder(); 1097 1098 // Remove the current alert message from the list. 1099 SmsCbMessage lastMessage = removeLatestMessage(); 1100 if (lastMessage == null) { 1101 Log.e(TAG, "dismiss() called with empty message list!"); 1102 finish(); 1103 return; 1104 } 1105 1106 // Remove the read message from the notification bar. 1107 // e.g, read the message from emergency alert history, need to update the notification bar. 1108 removeReadMessageFromNotificationBar(lastMessage, getApplicationContext()); 1109 1110 // Mark the alert as read. 1111 final long deliveryTime = lastMessage.getReceivedTime(); 1112 1113 // Mark broadcast as read on a background thread. 1114 new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver()) 1115 .execute((CellBroadcastContentProvider.CellBroadcastOperation) provider 1116 -> provider.markBroadcastRead(Telephony.CellBroadcasts.DELIVERY_TIME, 1117 deliveryTime)); 1118 1119 // Set the opt-out dialog flag if this is a CMAS alert (other than Always-on alert e.g, 1120 // Presidential alert). 1121 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 1122 getApplicationContext(), 1123 lastMessage.getSubscriptionId()); 1124 CellBroadcastChannelRange range = channelManager 1125 .getCellBroadcastChannelRangeFromMessage(lastMessage); 1126 1127 if (!neverShowOptOutDialog(lastMessage.getSubscriptionId()) && range != null 1128 && !range.mAlwaysOn) { 1129 mShowOptOutDialog = true; 1130 } 1131 1132 // If there are older emergency alerts to display, update the alert text and return. 1133 SmsCbMessage nextMessage = getLatestMessage(); 1134 if (nextMessage != null) { 1135 setFinishAlertOnTouchOutside(); 1136 updateAlertText(nextMessage); 1137 int subId = nextMessage.getSubscriptionId(); 1138 if (channelManager.isEmergencyMessage(nextMessage) 1139 && (range!= null && range.mDisplayIcon)) { 1140 mAnimationHandler.startIconAnimation(subId); 1141 } else { 1142 mAnimationHandler.stopIconAnimation(); 1143 } 1144 return; 1145 } 1146 1147 // Remove pending screen-off messages (animation messages are removed in onPause()). 1148 mScreenOffHandler.stopScreenOnTimer(); 1149 1150 // Show opt-in/opt-out dialog when the first CMAS alert is received. 1151 if (mShowOptOutDialog) { 1152 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 1153 if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) { 1154 // Clear the flag so the user will only see the opt-out dialog once. 1155 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false) 1156 .apply(); 1157 1158 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 1159 if (km.inKeyguardRestrictedInputMode()) { 1160 Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)"); 1161 Intent intent = new Intent(this, CellBroadcastOptOutActivity.class); 1162 startActivity(intent); 1163 } else { 1164 Log.d(TAG, "Showing opt-out dialog in current activity"); 1165 mOptOutDialog = CellBroadcastOptOutActivity.showOptOutDialog(this); 1166 return; // don't call finish() until user dismisses the dialog 1167 } 1168 } 1169 } 1170 finish(); 1171 } 1172 1173 @Override onDestroy()1174 public void onDestroy() { 1175 try { 1176 unregisterReceiver(mScreenOffReceiver); 1177 } catch (IllegalArgumentException e) { 1178 Log.e(TAG, "Unregister Receiver fail", e); 1179 } 1180 super.onDestroy(); 1181 } 1182 1183 @Override onKeyDown(int keyCode, KeyEvent event)1184 public boolean onKeyDown(int keyCode, KeyEvent event) { 1185 Log.d(TAG, "onKeyDown: " + event); 1186 SmsCbMessage message = getLatestMessage(); 1187 if (message != null && CellBroadcastSettings.getResourcesByOperator(getApplicationContext(), 1188 message.getSubscriptionId(), 1189 CellBroadcastReceiver.getRoamingOperatorSupported(getApplicationContext())) 1190 .getBoolean(R.bool.mute_by_physical_button)) { 1191 switch (event.getKeyCode()) { 1192 // Volume keys and camera keys mute the alert sound/vibration (except ETWS). 1193 case KeyEvent.KEYCODE_VOLUME_UP: 1194 case KeyEvent.KEYCODE_VOLUME_DOWN: 1195 case KeyEvent.KEYCODE_VOLUME_MUTE: 1196 case KeyEvent.KEYCODE_CAMERA: 1197 case KeyEvent.KEYCODE_FOCUS: 1198 // Stop playing alert sound/vibration/speech (if started) 1199 stopService(new Intent(this, CellBroadcastAlertAudio.class)); 1200 return true; 1201 1202 default: 1203 break; 1204 } 1205 return super.onKeyDown(keyCode, event); 1206 } else { 1207 if (event.getKeyCode() == KeyEvent.KEYCODE_POWER) { 1208 // TODO: do something to prevent screen off 1209 } 1210 // Disable all physical keys if mute_by_physical_button is false 1211 return true; 1212 } 1213 } 1214 1215 @Override onBackPressed()1216 public void onBackPressed() { 1217 // Disable back key 1218 } 1219 1220 /** 1221 * Hide opt-out dialog. 1222 * In case of any emergency alert invisible, need to hide the opt-out dialog when 1223 * new alert coming. 1224 */ hideOptOutDialog()1225 private void hideOptOutDialog() { 1226 if (mOptOutDialog != null && mOptOutDialog.isShowing()) { 1227 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 1228 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true) 1229 .apply(); 1230 mOptOutDialog.dismiss(); 1231 } 1232 } 1233 1234 /** 1235 * @return true if the device is configured to never show the opt out dialog for the mcc/mnc 1236 */ neverShowOptOutDialog(int subId)1237 private boolean neverShowOptOutDialog(int subId) { 1238 return CellBroadcastSettings.getResourcesByOperator(getApplicationContext(), subId, 1239 CellBroadcastReceiver.getRoamingOperatorSupported(getApplicationContext())) 1240 .getBoolean(R.bool.disable_opt_out_dialog); 1241 } 1242 1243 /** 1244 * Copy the message to clipboard. 1245 * 1246 * @param message Cell broadcast message. 1247 * 1248 * @return {@code true} if success, otherwise {@code false}; 1249 */ 1250 @VisibleForTesting copyMessageToClipboard(SmsCbMessage message, Context context)1251 public static boolean copyMessageToClipboard(SmsCbMessage message, Context context) { 1252 ClipboardManager cm = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); 1253 if (cm == null) return false; 1254 1255 cm.setPrimaryClip(ClipData.newPlainText("Alert Message", message.getMessageBody())); 1256 1257 String msg = CellBroadcastSettings.getResourcesByOperator(context, 1258 message.getSubscriptionId(), 1259 CellBroadcastReceiver.getRoamingOperatorSupported(context)) 1260 .getString(R.string.message_copied); 1261 Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); 1262 return true; 1263 } 1264 1265 /** 1266 * Remove read message from the notification bar, update the notification text, count or cancel 1267 * the notification if there is no un-read messages. 1268 * @param message The dismissed/read message to be removed from the notification bar 1269 * @param context 1270 */ removeReadMessageFromNotificationBar(SmsCbMessage message, Context context)1271 private void removeReadMessageFromNotificationBar(SmsCbMessage message, Context context) { 1272 Log.d(TAG, "removeReadMessageFromNotificationBar, msg: " + message.toString()); 1273 ArrayList<SmsCbMessage> unreadMessageList = CellBroadcastReceiverApp 1274 .removeReadMessage(message); 1275 if (unreadMessageList.isEmpty()) { 1276 Log.d(TAG, "removeReadMessageFromNotificationBar, cancel notification"); 1277 NotificationManager notificationManager = getSystemService(NotificationManager.class); 1278 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); 1279 } else { 1280 Log.d(TAG, "removeReadMessageFromNotificationBar, update count to " 1281 + unreadMessageList.size() ); 1282 // do not alert if remove unread messages from the notification bar. 1283 CellBroadcastAlertService.addToNotificationBar( 1284 CellBroadcastReceiverApp.getLatestMessage(), 1285 unreadMessageList, context,false, false, false); 1286 } 1287 } 1288 1289 /** 1290 * Finish alert dialog only if all messages are configured with DismissOnOutsideTouch. 1291 * When multiple messages are displayed, the message with dismissOnOutsideTouch(normally low 1292 * priority message) is displayed on top of other unread alerts without dismissOnOutsideTouch, 1293 * users can easily dismiss all messages by touching the screen. better way is to dismiss the 1294 * alert if and only if all messages with dismiss_on_outside_touch set true. 1295 */ setFinishAlertOnTouchOutside()1296 private void setFinishAlertOnTouchOutside() { 1297 if (mMessageList != null) { 1298 int dismissCount = 0; 1299 for (SmsCbMessage message : mMessageList) { 1300 CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( 1301 this, message.getSubscriptionId()); 1302 CellBroadcastChannelManager.CellBroadcastChannelRange range = 1303 channelManager.getCellBroadcastChannelRangeFromMessage(message); 1304 if (range != null && range.mDismissOnOutsideTouch) { 1305 dismissCount++; 1306 } 1307 } 1308 setFinishOnTouchOutside(mMessageList.size() > 0 && mMessageList.size() == dismissCount); 1309 } 1310 } 1311 1312 /** 1313 * If message list of dialog does not have message which is included in newMessageList, 1314 * Create new list which includes both dialogMessageList and newMessageList 1315 * without the duplicated message, and Return the new list. 1316 * If not, just return dialogMessageList as default. 1317 * @param dialogMessageList message list which this dialog activity is having 1318 * @param newMessageList message list which is compared with dialogMessageList 1319 * @return message list which is created with dialogMessageList and newMessageList 1320 */ 1321 @VisibleForTesting getNewMessageListIfNeeded( ArrayList<SmsCbMessage> dialogMessageList, ArrayList<SmsCbMessage> newMessageList)1322 public ArrayList<SmsCbMessage> getNewMessageListIfNeeded( 1323 ArrayList<SmsCbMessage> dialogMessageList, 1324 ArrayList<SmsCbMessage> newMessageList) { 1325 if (newMessageList == null || dialogMessageList == null) { 1326 return dialogMessageList; 1327 } 1328 ArrayList<SmsCbMessage> clonedNewMessageList = new ArrayList<>(newMessageList); 1329 for (SmsCbMessage message : dialogMessageList) { 1330 clonedNewMessageList.removeIf( 1331 msg -> msg.getReceivedTime() == message.getReceivedTime()); 1332 } 1333 Log.d(TAG, "clonedMessageList.size()=" + clonedNewMessageList.size()); 1334 if (clonedNewMessageList.size() > 0) { 1335 ArrayList<SmsCbMessage> resultList = new ArrayList<>(dialogMessageList); 1336 resultList.addAll(clonedNewMessageList); 1337 Comparator<SmsCbMessage> comparator = (Comparator) (o1, o2) -> { 1338 Long time1 = new Long(((SmsCbMessage) o1).getReceivedTime()); 1339 Long time2 = new Long(((SmsCbMessage) o2).getReceivedTime()); 1340 return time1.compareTo(time2); 1341 }; 1342 if (CellBroadcastSettings.getResourcesForDefaultSubId(getApplicationContext()) 1343 .getBoolean(R.bool.show_cmas_messages_in_priority_order)) { 1344 Log.d(TAG, "Use priority order Based Comparator"); 1345 comparator = mPriorityBasedComparator; 1346 } 1347 Collections.sort(resultList, comparator); 1348 return resultList; 1349 } 1350 return dialogMessageList; 1351 } 1352 1353 /** 1354 * To disable navigation bars, quick settings etc. Force users to engage with the alert dialog 1355 * before switching to other activities. 1356 * 1357 * @param disable if set to {@code true} to disable the status bar. {@code false} otherwise. 1358 */ setStatusBarDisabledIfNeeded(boolean disable)1359 private void setStatusBarDisabledIfNeeded(boolean disable) { 1360 if (!CellBroadcastSettings.getResourcesForDefaultSubId(getApplicationContext()) 1361 .getBoolean(R.bool.disable_status_bar)) { 1362 return; 1363 } 1364 try { 1365 // TODO change to system API in future. 1366 StatusBarManager statusBarManager = getSystemService(StatusBarManager.class); 1367 Method disableMethod = StatusBarManager.class.getDeclaredMethod( 1368 "disable", int.class); 1369 Method disableMethod2 = StatusBarManager.class.getDeclaredMethod( 1370 "disable2", int.class); 1371 if (disable) { 1372 // flags to be disabled 1373 int disableHome = StatusBarManager.class.getDeclaredField("DISABLE_HOME") 1374 .getInt(null); 1375 int disableRecent = StatusBarManager.class 1376 .getDeclaredField("DISABLE_RECENT").getInt(null); 1377 int disableBack = StatusBarManager.class.getDeclaredField("DISABLE_BACK") 1378 .getInt(null); 1379 int disableQuickSettings = StatusBarManager.class.getDeclaredField( 1380 "DISABLE2_QUICK_SETTINGS").getInt(null); 1381 int disableNotificationShaded = StatusBarManager.class.getDeclaredField( 1382 "DISABLE2_NOTIFICATION_SHADE").getInt(null); 1383 disableMethod.invoke(statusBarManager, disableHome | disableBack | disableRecent); 1384 disableMethod2.invoke(statusBarManager, disableQuickSettings 1385 | disableNotificationShaded); 1386 } else { 1387 int disableNone = StatusBarManager.class.getDeclaredField("DISABLE_NONE") 1388 .getInt(null); 1389 disableMethod.invoke(statusBarManager, disableNone); 1390 disableMethod2.invoke(statusBarManager, disableNone); 1391 } 1392 } catch (Exception e) { 1393 CellBroadcastReceiverMetrics.getInstance() 1394 .logModuleError(ERRSRC_CBR, ERRTYPE_STATUSBAR); 1395 Log.e(TAG, "Failed to disable navigation when showing alert: ", e); 1396 } 1397 } 1398 } 1399