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