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