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