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