1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.cellbroadcastreceiver;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.KeyguardManager;
24 import android.app.NotificationManager;
25 import android.content.ClipData;
26 import android.content.ClipboardManager;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.res.Configuration;
31 import android.content.res.Resources;
32 import android.graphics.Point;
33 import android.graphics.drawable.Drawable;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Message;
37 import android.os.PowerManager;
38 import android.preference.PreferenceManager;
39 import android.provider.Telephony;
40 import android.telephony.SmsCbCmasInfo;
41 import android.telephony.SmsCbMessage;
42 import android.telephony.SubscriptionManager;
43 import android.text.Spannable;
44 import android.text.SpannableString;
45 import android.text.format.DateUtils;
46 import android.text.method.LinkMovementMethod;
47 import android.text.util.Linkify;
48 import android.util.Log;
49 import android.view.Display;
50 import android.view.KeyEvent;
51 import android.view.LayoutInflater;
52 import android.view.View;
53 import android.view.ViewGroup;
54 import android.view.Window;
55 import android.view.WindowManager;
56 import android.view.textclassifier.TextClassifier;
57 import android.view.textclassifier.TextLinks;
58 import android.widget.ImageView;
59 import android.widget.TextView;
60 import android.widget.Toast;
61 
62 import com.android.internal.annotations.VisibleForTesting;
63 
64 import java.lang.annotation.Retention;
65 import java.lang.annotation.RetentionPolicy;
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.Collections;
69 import java.util.Comparator;
70 import java.util.concurrent.atomic.AtomicInteger;
71 
72 /**
73  * Custom alert dialog with optional flashing warning icon.
74  * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}.
75  */
76 public class CellBroadcastAlertDialog extends Activity {
77 
78     private static final String TAG = "CellBroadcastAlertDialog";
79 
80     /** Intent extra for non-emergency alerts sent when user selects the notification. */
81     @VisibleForTesting
82     public static final String FROM_NOTIFICATION_EXTRA = "from_notification";
83 
84     // Intent extra to identify if notification was sent while trying to move away from the dialog
85     //  without acknowledging the dialog
86     static final String FROM_SAVE_STATE_NOTIFICATION_EXTRA = "from_save_state_notification";
87 
88     /** Not link any text. */
89     private static final int LINK_METHOD_NONE = 0;
90 
91     private static final String LINK_METHOD_NONE_STRING = "none";
92 
93     /** Use {@link android.text.util.Linkify} to generate links. */
94     private static final int LINK_METHOD_LEGACY_LINKIFY = 1;
95 
96     private static final String LINK_METHOD_LEGACY_LINKIFY_STRING = "legacy_linkify";
97 
98     /**
99      * Use the machine learning based {@link TextClassifier} to generate links. Will fallback to
100      * {@link #LINK_METHOD_LEGACY_LINKIFY} if not enabled.
101      */
102     private static final int LINK_METHOD_SMART_LINKIFY = 2;
103 
104     private static final String LINK_METHOD_SMART_LINKIFY_STRING = "smart_linkify";
105 
106     /**
107      * Text link method
108      * @hide
109      */
110     @Retention(RetentionPolicy.SOURCE)
111     @IntDef(prefix = "LINK_METHOD_",
112             value = {LINK_METHOD_NONE, LINK_METHOD_LEGACY_LINKIFY,
113                     LINK_METHOD_SMART_LINKIFY})
114     private @interface LinkMethod {}
115 
116 
117     /** List of cell broadcast messages to display (oldest to newest). */
118     protected ArrayList<SmsCbMessage> mMessageList;
119 
120     /** Whether a CMAS alert other than Presidential Alert was displayed. */
121     private boolean mShowOptOutDialog;
122 
123     /** Length of time for the warning icon to be visible. */
124     private static final int WARNING_ICON_ON_DURATION_MSEC = 800;
125 
126     /** Length of time for the warning icon to be off. */
127     private static final int WARNING_ICON_OFF_DURATION_MSEC = 800;
128 
129     /** Length of time to keep the screen turned on. */
130     private static final int KEEP_SCREEN_ON_DURATION_MSEC = 60000;
131 
132     /** Animation handler for the flashing warning icon (emergency alerts only). */
133     @VisibleForTesting
134     public AnimationHandler mAnimationHandler = new AnimationHandler();
135 
136     /** Handler to add and remove screen on flags for emergency alerts. */
137     private final ScreenOffHandler mScreenOffHandler = new ScreenOffHandler();
138 
139     // Show the opt-out dialog
140     private AlertDialog mOptOutDialog;
141 
142     /**
143      * Animation handler for the flashing warning icon (emergency alerts only).
144      */
145     @VisibleForTesting
146     public class AnimationHandler extends Handler {
147         /** Latest {@code message.what} value for detecting old messages. */
148         @VisibleForTesting
149         public final AtomicInteger mCount = new AtomicInteger();
150 
151         /** Warning icon state: visible == true, hidden == false. */
152         @VisibleForTesting
153         public boolean mWarningIconVisible;
154 
155         /** The warning icon Drawable. */
156         private Drawable mWarningIcon;
157 
158         /** The View containing the warning icon. */
159         private ImageView mWarningIconView;
160 
161         /** Package local constructor (called from outer class). */
AnimationHandler()162         AnimationHandler() {}
163 
164         /** Start the warning icon animation. */
165         @VisibleForTesting
startIconAnimation(int subId)166         public void startIconAnimation(int subId) {
167             if (!initDrawableAndImageView(subId)) {
168                 return;     // init failure
169             }
170             mWarningIconVisible = true;
171             mWarningIconView.setVisibility(View.VISIBLE);
172             updateIconState();
173             queueAnimateMessage();
174         }
175 
176         /** Stop the warning icon animation. */
177         @VisibleForTesting
stopIconAnimation()178         public void stopIconAnimation() {
179             // Increment the counter so the handler will ignore the next message.
180             mCount.incrementAndGet();
181             if (mWarningIconView != null) {
182                 mWarningIconView.setVisibility(View.GONE);
183             }
184         }
185 
186         /** Update the visibility of the warning icon. */
updateIconState()187         private void updateIconState() {
188             mWarningIconView.setImageAlpha(mWarningIconVisible ? 255 : 0);
189             mWarningIconView.invalidateDrawable(mWarningIcon);
190         }
191 
192         /** Queue a message to animate the warning icon. */
queueAnimateMessage()193         private void queueAnimateMessage() {
194             int msgWhat = mCount.incrementAndGet();
195             sendEmptyMessageDelayed(msgWhat, mWarningIconVisible ? WARNING_ICON_ON_DURATION_MSEC
196                     : WARNING_ICON_OFF_DURATION_MSEC);
197         }
198 
199         @Override
handleMessage(Message msg)200         public void handleMessage(Message msg) {
201             if (msg.what == mCount.get()) {
202                 mWarningIconVisible = !mWarningIconVisible;
203                 updateIconState();
204                 queueAnimateMessage();
205             }
206         }
207 
208         /**
209          * Initialize the Drawable and ImageView fields.
210          *
211          * @param subId Subscription index
212          *
213          * @return true if successful; false if any field failed to initialize
214          */
initDrawableAndImageView(int subId)215         private boolean initDrawableAndImageView(int subId) {
216             if (mWarningIcon == null) {
217                 try {
218                     mWarningIcon = CellBroadcastSettings.getResources(getApplicationContext(),
219                             subId).getDrawable(R.drawable.ic_warning_googred);
220                 } catch (Resources.NotFoundException e) {
221                     Log.e(TAG, "warning icon resource not found", e);
222                     return false;
223                 }
224             }
225             if (mWarningIconView == null) {
226                 mWarningIconView = (ImageView) findViewById(R.id.icon);
227                 if (mWarningIconView != null) {
228                     mWarningIconView.setImageDrawable(mWarningIcon);
229                 } else {
230                     Log.e(TAG, "failed to get ImageView for warning icon");
231                     return false;
232                 }
233             }
234             return true;
235         }
236     }
237 
238     /**
239      * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay,
240      * remove the flag so the screen can turn off to conserve the battery.
241      */
242     private class ScreenOffHandler extends Handler {
243         /** Latest {@code message.what} value for detecting old messages. */
244         private final AtomicInteger mCount = new AtomicInteger();
245 
246         /** Package local constructor (called from outer class). */
ScreenOffHandler()247         ScreenOffHandler() {}
248 
249         /** Add screen on window flags and queue a delayed message to remove them later. */
startScreenOnTimer()250         void startScreenOnTimer() {
251             addWindowFlags();
252             int msgWhat = mCount.incrementAndGet();
253             removeMessages(msgWhat - 1);    // Remove previous message, if any.
254             sendEmptyMessageDelayed(msgWhat, KEEP_SCREEN_ON_DURATION_MSEC);
255             Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat);
256         }
257 
258         /** Remove the screen on window flags and any queued screen off message. */
stopScreenOnTimer()259         void stopScreenOnTimer() {
260             removeMessages(mCount.get());
261             clearWindowFlags();
262         }
263 
264         /** Set the screen on window flags. */
addWindowFlags()265         private void addWindowFlags() {
266             getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
267                     | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
268         }
269 
270         /** Clear the screen on window flags. */
clearWindowFlags()271         private void clearWindowFlags() {
272             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
273                     | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
274         }
275 
276         @Override
handleMessage(Message msg)277         public void handleMessage(Message msg) {
278             int msgWhat = msg.what;
279             if (msgWhat == mCount.get()) {
280                 clearWindowFlags();
281                 Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat);
282             } else {
283                 Log.e(TAG, "discarding screen off message with id " + msgWhat);
284             }
285         }
286     }
287 
288     @Override
onCreate(Bundle savedInstanceState)289     protected void onCreate(Bundle savedInstanceState) {
290         super.onCreate(savedInstanceState);
291 
292         final Window win = getWindow();
293 
294         // We use a custom title, so remove the standard dialog title bar
295         win.requestFeature(Window.FEATURE_NO_TITLE);
296 
297         // Full screen alerts display above the keyguard and when device is locked.
298         win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
299                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
300                 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
301 
302         // Disable home button when alert dialog is showing if mute_by_physical_button is false.
303         if (!CellBroadcastSettings.getResources(getApplicationContext(),
304                 SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
305                 .getBoolean(R.bool.mute_by_physical_button)) {
306             final View decorView = win.getDecorView();
307             decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
308         }
309 
310         setFinishOnTouchOutside(false);
311 
312         // Initialize the view.
313         LayoutInflater inflater = LayoutInflater.from(this);
314         setContentView(inflater.inflate(R.layout.cell_broadcast_alert, null));
315 
316         findViewById(R.id.dismissButton).setOnClickListener(v -> dismiss());
317 
318         // Get message list from saved Bundle or from Intent.
319         if (savedInstanceState != null) {
320             Log.d(TAG, "onCreate getting message list from saved instance state");
321             mMessageList = savedInstanceState.getParcelableArrayList(
322                     CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA);
323         } else {
324             Log.d(TAG, "onCreate getting message list from intent");
325             Intent intent = getIntent();
326             mMessageList = intent.getParcelableArrayListExtra(
327                     CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA);
328 
329             // If we were started from a notification, dismiss it.
330             clearNotification(intent);
331         }
332 
333         if (mMessageList == null || mMessageList.size() == 0) {
334             Log.e(TAG, "onCreate failed as message list is null or empty");
335             finish();
336         } else {
337             Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size());
338 
339             // For emergency alerts, keep screen on so the user can read it
340             SmsCbMessage message = getLatestMessage();
341 
342             if (message == null) {
343                 Log.e(TAG, "message is null");
344                 finish();
345                 return;
346             }
347 
348             CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(
349                     this, message.getSubscriptionId());
350             if (channelManager.isEmergencyMessage(message)) {
351                 Log.d(TAG, "onCreate setting screen on timer for emergency alert for sub "
352                         + message.getSubscriptionId());
353                 mScreenOffHandler.startScreenOnTimer();
354             }
355 
356             updateAlertText(message);
357 
358             Resources res = CellBroadcastSettings.getResources(getApplicationContext(),
359                     message.getSubscriptionId());
360             if (res.getBoolean(R.bool.enable_text_copy)) {
361                 TextView textView = findViewById(R.id.message);
362                 if (textView != null) {
363                     textView.setOnLongClickListener(v -> copyMessageToClipboard(message,
364                             getApplicationContext()));
365                 }
366             }
367         }
368     }
369 
370     @Override
onStart()371     public void onStart() {
372         super.onStart();
373         getWindow().addSystemFlags(
374                 android.view.WindowManager.LayoutParams
375                         .SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
376     }
377 
378     /**
379      * Start animating warning icon.
380      */
381     @Override
382     @VisibleForTesting
onResume()383     public void onResume() {
384         super.onResume();
385         SmsCbMessage message = getLatestMessage();
386         if (message != null) {
387             int subId = message.getSubscriptionId();
388             CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(this,
389                     subId);
390             if (channelManager.isEmergencyMessage(message)) {
391                 mAnimationHandler.startIconAnimation(subId);
392             }
393         }
394     }
395 
396     /**
397      * Stop animating warning icon.
398      */
399     @Override
400     @VisibleForTesting
onPause()401     public void onPause() {
402         Log.d(TAG, "onPause called");
403         mAnimationHandler.stopIconAnimation();
404         super.onPause();
405     }
406 
407     @Override
onStop()408     protected void onStop() {
409         Log.d(TAG, "onStop called");
410         // When the activity goes in background eg. clicking Home button, send notification.
411         // Avoid doing this when activity will be recreated because of orientation change or if
412         // screen goes off
413         PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
414         if (!(isChangingConfigurations() || getLatestMessage() == null) && pm.isScreenOn()) {
415             CellBroadcastAlertService.addToNotificationBar(getLatestMessage(), mMessageList,
416                     getApplicationContext(), true);
417         }
418         // Stop playing alert sound/vibration/speech (if started)
419         stopService(new Intent(this, CellBroadcastAlertAudio.class));
420         super.onStop();
421     }
422 
423     @Override
onWindowFocusChanged(boolean hasFocus)424     public void onWindowFocusChanged(boolean hasFocus) {
425         super.onWindowFocusChanged(hasFocus);
426 
427         if (hasFocus) {
428             Configuration config = getResources().getConfiguration();
429             setPictogramAreaLayout(config.orientation);
430         }
431     }
432 
433     @Override
onConfigurationChanged(Configuration newConfig)434     public void onConfigurationChanged(Configuration newConfig) {
435         super.onConfigurationChanged(newConfig);
436         setPictogramAreaLayout(newConfig.orientation);
437     }
438 
439     /** Returns the currently displayed message. */
getLatestMessage()440     SmsCbMessage getLatestMessage() {
441         int index = mMessageList.size() - 1;
442         if (index >= 0) {
443             return mMessageList.get(index);
444         } else {
445             Log.d(TAG, "getLatestMessage returns null");
446             return null;
447         }
448     }
449 
450     /** Removes and returns the currently displayed message. */
removeLatestMessage()451     private SmsCbMessage removeLatestMessage() {
452         int index = mMessageList.size() - 1;
453         if (index >= 0) {
454             return mMessageList.remove(index);
455         } else {
456             return null;
457         }
458     }
459 
460     /**
461      * Save the list of messages so the state can be restored later.
462      * @param outState Bundle in which to place the saved state.
463      */
464     @Override
onSaveInstanceState(Bundle outState)465     protected void onSaveInstanceState(Bundle outState) {
466         super.onSaveInstanceState(outState);
467         outState.putParcelableArrayList(
468                 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, mMessageList);
469     }
470 
471     /**
472      * Get link method
473      *
474      * @param subId Subscription index
475      * @return The link method
476      */
getLinkMethod(int subId)477     private @LinkMethod int getLinkMethod(int subId) {
478         Resources res = CellBroadcastSettings.getResources(getApplicationContext(), subId);
479         switch (res.getString(R.string.link_method)) {
480             case LINK_METHOD_NONE_STRING: return LINK_METHOD_NONE;
481             case LINK_METHOD_LEGACY_LINKIFY_STRING: return LINK_METHOD_LEGACY_LINKIFY;
482             case LINK_METHOD_SMART_LINKIFY_STRING: return LINK_METHOD_SMART_LINKIFY;
483         }
484         return LINK_METHOD_NONE;
485     }
486 
487     /**
488      * Add URL links to the applicable texts.
489      *
490      * @param textView Text view
491      * @param messageText The text string of the message
492      * @param linkMethod Link method
493      */
addLinks(@onNull TextView textView, @NonNull String messageText, @LinkMethod int linkMethod)494     private void addLinks(@NonNull TextView textView, @NonNull String messageText,
495             @LinkMethod int linkMethod) {
496         Spannable text = new SpannableString(messageText);
497         if (linkMethod == LINK_METHOD_LEGACY_LINKIFY) {
498             Linkify.addLinks(text, Linkify.ALL);
499             textView.setMovementMethod(LinkMovementMethod.getInstance());
500             textView.setText(text);
501         } else if (linkMethod == LINK_METHOD_SMART_LINKIFY) {
502             // Text classification cannot be run in the main thread.
503             new Thread(() -> {
504                 final TextClassifier classifier = textView.getTextClassifier();
505 
506                 TextClassifier.EntityConfig entityConfig =
507                         new TextClassifier.EntityConfig.Builder()
508                                 .setIncludedTypes(Arrays.asList(
509                                         TextClassifier.TYPE_URL,
510                                         TextClassifier.TYPE_EMAIL,
511                                         TextClassifier.TYPE_PHONE,
512                                         TextClassifier.TYPE_ADDRESS,
513                                         TextClassifier.TYPE_FLIGHT_NUMBER))
514                                 .setExcludedTypes(Arrays.asList(
515                                         TextClassifier.TYPE_DATE,
516                                         TextClassifier.TYPE_DATE_TIME))
517                                 .build();
518 
519                 TextLinks.Request request = new TextLinks.Request.Builder(text)
520                         .setEntityConfig(entityConfig)
521                         .build();
522                 // Add links to the spannable text.
523                 classifier.generateLinks(request).apply(
524                         text, TextLinks.APPLY_STRATEGY_REPLACE, null);
525 
526                 // UI can be only updated in the main thread.
527                 runOnUiThread(() -> {
528                     textView.setMovementMethod(LinkMovementMethod.getInstance());
529                     textView.setText(text);
530                 });
531             }).start();
532         }
533     }
534 
535     /**
536      * Update alert text when a new emergency alert arrives.
537      * @param message CB message which is used to update alert text.
538      */
updateAlertText(@onNull SmsCbMessage message)539     private void updateAlertText(@NonNull SmsCbMessage message) {
540         Context context = getApplicationContext();
541         int titleId = CellBroadcastResources.getDialogTitleResource(context, message);
542 
543         String title = getText(titleId).toString();
544         TextView titleTextView = findViewById(R.id.alertTitle);
545 
546         Resources res = CellBroadcastSettings.getResources(context, message.getSubscriptionId());
547         if (titleTextView != null) {
548             if (res.getBoolean(R.bool.show_date_time_title)) {
549                 titleTextView.setSingleLine(false);
550                 title += "\n" + DateUtils.formatDateTime(context, message.getReceivedTime(),
551                         DateUtils.FORMAT_NO_NOON_MIDNIGHT | DateUtils.FORMAT_SHOW_TIME
552                                 | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE
553                                 | DateUtils.FORMAT_CAP_AMPM);
554             }
555 
556             setTitle(title);
557             titleTextView.setText(title);
558         }
559 
560         TextView textView = findViewById(R.id.message);
561         String messageText = message.getMessageBody();
562         if (textView != null && messageText != null) {
563             int linkMethod = getLinkMethod(message.getSubscriptionId());
564             if (linkMethod != LINK_METHOD_NONE) {
565                 addLinks(textView, messageText, linkMethod);
566             } else {
567                 // Do not add any link to the message text.
568                 textView.setText(messageText);
569             }
570         }
571 
572         String dismissButtonText = getString(R.string.button_dismiss);
573 
574         if (mMessageList.size() > 1) {
575             dismissButtonText += "  (1/" + mMessageList.size() + ")";
576         }
577 
578         ((TextView) findViewById(R.id.dismissButton)).setText(dismissButtonText);
579 
580 
581         setPictogram(context, message);
582     }
583 
584     /**
585      * Set pictogram image
586      * @param context
587      * @param message
588      */
setPictogram(Context context, SmsCbMessage message)589     private void setPictogram(Context context, SmsCbMessage message) {
590         int resId = CellBroadcastResources.getDialogPictogramResource(context, message);
591         ImageView image = findViewById(R.id.pictogramImage);
592         if (resId != -1) {
593             image.setImageResource(resId);
594             image.setVisibility(View.VISIBLE);
595         } else {
596             image.setVisibility(View.GONE);
597         }
598     }
599 
600     /**
601      * Set pictogram to match orientation
602      *
603      * @param orientation The orientation of the pictogram.
604      */
setPictogramAreaLayout(int orientation)605     private void setPictogramAreaLayout(int orientation) {
606         ImageView image = findViewById(R.id.pictogramImage);
607         if (image.getVisibility() == View.VISIBLE) {
608             ViewGroup.LayoutParams params = image.getLayoutParams();
609 
610             if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
611                 Display display = getWindowManager().getDefaultDisplay();
612                 Point point = new Point();
613                 display.getSize(point);
614                 params.width = (int) (point.x * 0.3);
615                 params.height = (int) (point.y * 0.3);
616             } else {
617                 params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
618                 params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
619             }
620 
621             image.setLayoutParams(params);
622         }
623     }
624 
625     /**
626      * Called by {@link CellBroadcastAlertService} to add a new alert to the stack.
627      * @param intent The new intent containing one or more {@link SmsCbMessage}.
628      */
629     @Override
630     @VisibleForTesting
onNewIntent(Intent intent)631     public void onNewIntent(Intent intent) {
632         ArrayList<SmsCbMessage> newMessageList = intent.getParcelableArrayListExtra(
633                 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA);
634         if (newMessageList != null) {
635             if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) {
636                 mMessageList = newMessageList;
637             } else {
638                 mMessageList.addAll(newMessageList);
639                 if (CellBroadcastSettings.getResources(getApplicationContext(),
640                         SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
641                         .getBoolean(R.bool.show_cmas_messages_in_priority_order)) {
642                     // Sort message list to show messages in a different order than received by
643                     // prioritizing them. Presidential Alert only has top priority.
644                     Collections.sort(
645                             mMessageList,
646                             (Comparator) (o1, o2) -> {
647                                 boolean isPresidentialAlert1 =
648                                         ((SmsCbMessage) o1).isCmasMessage()
649                                                 && ((SmsCbMessage) o1).getCmasWarningInfo()
650                                                 .getMessageClass() == SmsCbCmasInfo
651                                                 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
652                                 boolean isPresidentialAlert2 =
653                                         ((SmsCbMessage) o2).isCmasMessage()
654                                                 && ((SmsCbMessage) o2).getCmasWarningInfo()
655                                                 .getMessageClass() == SmsCbCmasInfo
656                                                 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
657                                 if (isPresidentialAlert1 ^ isPresidentialAlert2) {
658                                     return isPresidentialAlert1 ? 1 : -1;
659                                 }
660                                 Long time1 =
661                                         new Long(((SmsCbMessage) o1).getReceivedTime());
662                                 Long time2 =
663                                         new Long(((SmsCbMessage) o2).getReceivedTime());
664                                 return time2.compareTo(time1);
665                             });
666                 }
667             }
668             Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size());
669             hideOptOutDialog(); // Hide opt-out dialog when new alert coming
670             updateAlertText(getLatestMessage());
671             // If the new intent was sent from a notification, dismiss it.
672             clearNotification(intent);
673         } else {
674             Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring");
675         }
676     }
677 
678     /**
679      * Try to cancel any notification that may have started this activity.
680      * @param intent Intent containing extras used to identify if notification needs to be cleared
681      */
clearNotification(Intent intent)682     private void clearNotification(Intent intent) {
683         if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
684             NotificationManager notificationManager =
685                     (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
686             notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
687             CellBroadcastReceiverApp.clearNewMessageList();
688         }
689     }
690 
691     /**
692      * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio}
693      * service if necessary.
694      */
695     @VisibleForTesting
dismiss()696     public void dismiss() {
697         Log.d(TAG, "dismiss");
698         // Stop playing alert sound/vibration/speech (if started)
699         stopService(new Intent(this, CellBroadcastAlertAudio.class));
700 
701         // Cancel any pending alert reminder
702         CellBroadcastAlertReminder.cancelAlertReminder();
703 
704         // Remove the current alert message from the list.
705         SmsCbMessage lastMessage = removeLatestMessage();
706         if (lastMessage == null) {
707             Log.e(TAG, "dismiss() called with empty message list!");
708             finish();
709             return;
710         }
711 
712         // Mark the alert as read.
713         final long deliveryTime = lastMessage.getReceivedTime();
714 
715         // Mark broadcast as read on a background thread.
716         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
717                 .execute((CellBroadcastContentProvider.CellBroadcastOperation) provider
718                         -> provider.markBroadcastRead(Telephony.CellBroadcasts.DELIVERY_TIME,
719                         deliveryTime));
720 
721         // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert).
722         if (lastMessage.isCmasMessage() && lastMessage.getCmasWarningInfo().getMessageClass()
723                 != SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) {
724             mShowOptOutDialog = true;
725         }
726 
727         // If there are older emergency alerts to display, update the alert text and return.
728         SmsCbMessage nextMessage = getLatestMessage();
729         if (nextMessage != null) {
730             updateAlertText(nextMessage);
731             int subId = nextMessage.getSubscriptionId();
732             CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(
733                     getApplicationContext(), subId);
734             if (channelManager.isEmergencyMessage(nextMessage)) {
735                 mAnimationHandler.startIconAnimation(subId);
736             } else {
737                 mAnimationHandler.stopIconAnimation();
738             }
739             return;
740         }
741 
742         // Remove pending screen-off messages (animation messages are removed in onPause()).
743         mScreenOffHandler.stopScreenOnTimer();
744 
745         // Show opt-in/opt-out dialog when the first CMAS alert is received.
746         if (mShowOptOutDialog) {
747             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
748             if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) {
749                 // Clear the flag so the user will only see the opt-out dialog once.
750                 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false)
751                         .apply();
752 
753                 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
754                 if (km.inKeyguardRestrictedInputMode()) {
755                     Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)");
756                     Intent intent = new Intent(this, CellBroadcastOptOutActivity.class);
757                     startActivity(intent);
758                 } else {
759                     Log.d(TAG, "Showing opt-out dialog in current activity");
760                     mOptOutDialog = CellBroadcastOptOutActivity.showOptOutDialog(this);
761                     return; // don't call finish() until user dismisses the dialog
762                 }
763             }
764         }
765         NotificationManager notificationManager =
766                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
767         notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
768         finish();
769     }
770 
771     @Override
onKeyDown(int keyCode, KeyEvent event)772     public boolean onKeyDown(int keyCode, KeyEvent event) {
773         Log.d(TAG, "onKeyDown: " + event);
774         SmsCbMessage message = getLatestMessage();
775         if (CellBroadcastSettings.getResources(getApplicationContext(), message.getSubscriptionId())
776                 .getBoolean(R.bool.mute_by_physical_button)) {
777             switch (event.getKeyCode()) {
778                 // Volume keys and camera keys mute the alert sound/vibration (except ETWS).
779                 case KeyEvent.KEYCODE_VOLUME_UP:
780                 case KeyEvent.KEYCODE_VOLUME_DOWN:
781                 case KeyEvent.KEYCODE_VOLUME_MUTE:
782                 case KeyEvent.KEYCODE_CAMERA:
783                 case KeyEvent.KEYCODE_FOCUS:
784                     // Stop playing alert sound/vibration/speech (if started)
785                     stopService(new Intent(this, CellBroadcastAlertAudio.class));
786                     return true;
787 
788                 default:
789                     break;
790             }
791             return super.onKeyDown(keyCode, event);
792         } else {
793             if (event.getKeyCode() == KeyEvent.KEYCODE_POWER) {
794                 // TODO: do something to prevent screen off
795             }
796             // Disable all physical keys if mute_by_physical_button is false
797             return true;
798         }
799     }
800 
801     @Override
onBackPressed()802     public void onBackPressed() {
803         // Disable back key
804     }
805 
806     /**
807      * Hide opt-out dialog.
808      * In case of any emergency alert invisible, need to hide the opt-out dialog when
809      * new alert coming.
810      */
hideOptOutDialog()811     private void hideOptOutDialog() {
812         if (mOptOutDialog != null && mOptOutDialog.isShowing()) {
813             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
814             prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)
815                     .apply();
816             mOptOutDialog.dismiss();
817         }
818     }
819 
820     /**
821      * Copy the message to clipboard.
822      *
823      * @param message Cell broadcast message.
824      *
825      * @return {@code true} if success, otherwise {@code false};
826      */
827     @VisibleForTesting
copyMessageToClipboard(SmsCbMessage message, Context context)828     public static boolean copyMessageToClipboard(SmsCbMessage message, Context context) {
829         ClipboardManager cm = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE);
830         if (cm == null) return false;
831 
832         cm.setPrimaryClip(ClipData.newPlainText("Alert Message", message.getMessageBody()));
833 
834         String msg = CellBroadcastSettings.getResources(context,
835                 message.getSubscriptionId()).getString(R.string.message_copied);
836         Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
837         return true;
838     }
839 }
840