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