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