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