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     /**
371      * Start animating warning icon.
372      */
373     @Override
374     @VisibleForTesting
onResume()375     public void onResume() {
376         super.onResume();
377         SmsCbMessage message = getLatestMessage();
378         if (message != null) {
379             int subId = message.getSubscriptionId();
380             CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(this,
381                     subId);
382             if (channelManager.isEmergencyMessage(message)) {
383                 mAnimationHandler.startIconAnimation(subId);
384             }
385         }
386     }
387 
388     /**
389      * Stop animating warning icon.
390      */
391     @Override
392     @VisibleForTesting
onPause()393     public void onPause() {
394         Log.d(TAG, "onPause called");
395         mAnimationHandler.stopIconAnimation();
396         super.onPause();
397     }
398 
399     @Override
onStop()400     protected void onStop() {
401         Log.d(TAG, "onStop called");
402         // When the activity goes in background eg. clicking Home button, send notification.
403         // Avoid doing this when activity will be recreated because of orientation change or if
404         // screen goes off
405         PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
406         if (!(isChangingConfigurations() || getLatestMessage() == null) && pm.isScreenOn()) {
407             CellBroadcastAlertService.addToNotificationBar(getLatestMessage(), mMessageList,
408                     getApplicationContext(), true);
409         }
410         // Stop playing alert sound/vibration/speech (if started)
411         stopService(new Intent(this, CellBroadcastAlertAudio.class));
412         super.onStop();
413     }
414 
415     @Override
onWindowFocusChanged(boolean hasFocus)416     public void onWindowFocusChanged(boolean hasFocus) {
417         super.onWindowFocusChanged(hasFocus);
418 
419         if (hasFocus) {
420             Configuration config = getResources().getConfiguration();
421             setPictogramAreaLayout(config.orientation);
422         }
423     }
424 
425     @Override
onConfigurationChanged(Configuration newConfig)426     public void onConfigurationChanged(Configuration newConfig) {
427         super.onConfigurationChanged(newConfig);
428         setPictogramAreaLayout(newConfig.orientation);
429     }
430 
431     /** Returns the currently displayed message. */
getLatestMessage()432     SmsCbMessage getLatestMessage() {
433         int index = mMessageList.size() - 1;
434         if (index >= 0) {
435             return mMessageList.get(index);
436         } else {
437             Log.d(TAG, "getLatestMessage returns null");
438             return null;
439         }
440     }
441 
442     /** Removes and returns the currently displayed message. */
removeLatestMessage()443     private SmsCbMessage removeLatestMessage() {
444         int index = mMessageList.size() - 1;
445         if (index >= 0) {
446             return mMessageList.remove(index);
447         } else {
448             return null;
449         }
450     }
451 
452     /**
453      * Save the list of messages so the state can be restored later.
454      * @param outState Bundle in which to place the saved state.
455      */
456     @Override
onSaveInstanceState(Bundle outState)457     protected void onSaveInstanceState(Bundle outState) {
458         super.onSaveInstanceState(outState);
459         outState.putParcelableArrayList(
460                 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, mMessageList);
461     }
462 
463     /**
464      * Get link method
465      *
466      * @param subId Subscription index
467      * @return The link method
468      */
getLinkMethod(int subId)469     private @LinkMethod int getLinkMethod(int subId) {
470         Resources res = CellBroadcastSettings.getResources(getApplicationContext(), subId);
471         switch (res.getString(R.string.link_method)) {
472             case LINK_METHOD_NONE_STRING: return LINK_METHOD_NONE;
473             case LINK_METHOD_LEGACY_LINKIFY_STRING: return LINK_METHOD_LEGACY_LINKIFY;
474             case LINK_METHOD_SMART_LINKIFY_STRING: return LINK_METHOD_SMART_LINKIFY;
475         }
476         return LINK_METHOD_NONE;
477     }
478 
479     /**
480      * Add URL links to the applicable texts.
481      *
482      * @param textView Text view
483      * @param messageText The text string of the message
484      * @param linkMethod Link method
485      */
addLinks(@onNull TextView textView, @NonNull String messageText, @LinkMethod int linkMethod)486     private void addLinks(@NonNull TextView textView, @NonNull String messageText,
487             @LinkMethod int linkMethod) {
488         Spannable text = new SpannableString(messageText);
489         if (linkMethod == LINK_METHOD_LEGACY_LINKIFY) {
490             Linkify.addLinks(text, Linkify.ALL);
491             textView.setMovementMethod(LinkMovementMethod.getInstance());
492             textView.setText(text);
493         } else if (linkMethod == LINK_METHOD_SMART_LINKIFY) {
494             // Text classification cannot be run in the main thread.
495             new Thread(() -> {
496                 final TextClassifier classifier = textView.getTextClassifier();
497 
498                 TextClassifier.EntityConfig entityConfig =
499                         new TextClassifier.EntityConfig.Builder()
500                                 .setIncludedTypes(Arrays.asList(
501                                         TextClassifier.TYPE_URL,
502                                         TextClassifier.TYPE_EMAIL,
503                                         TextClassifier.TYPE_PHONE,
504                                         TextClassifier.TYPE_ADDRESS,
505                                         TextClassifier.TYPE_FLIGHT_NUMBER))
506                                 .setExcludedTypes(Arrays.asList(
507                                         TextClassifier.TYPE_DATE,
508                                         TextClassifier.TYPE_DATE_TIME))
509                                 .build();
510 
511                 TextLinks.Request request = new TextLinks.Request.Builder(text)
512                         .setEntityConfig(entityConfig)
513                         .build();
514                 // Add links to the spannable text.
515                 classifier.generateLinks(request).apply(
516                         text, TextLinks.APPLY_STRATEGY_REPLACE, null);
517 
518                 // UI can be only updated in the main thread.
519                 runOnUiThread(() -> {
520                     textView.setMovementMethod(LinkMovementMethod.getInstance());
521                     textView.setText(text);
522                 });
523             }).start();
524         }
525     }
526 
527     /**
528      * Update alert text when a new emergency alert arrives.
529      * @param message CB message which is used to update alert text.
530      */
updateAlertText(@onNull SmsCbMessage message)531     private void updateAlertText(@NonNull SmsCbMessage message) {
532         Context context = getApplicationContext();
533         int titleId = CellBroadcastResources.getDialogTitleResource(context, message);
534 
535         String title = getText(titleId).toString();
536         TextView titleTextView = findViewById(R.id.alertTitle);
537 
538         Resources res = CellBroadcastSettings.getResources(context, message.getSubscriptionId());
539         if (titleTextView != null) {
540             if (res.getBoolean(R.bool.show_date_time_title)) {
541                 titleTextView.setSingleLine(false);
542                 title += "\n" + DateUtils.formatDateTime(context, message.getReceivedTime(),
543                         DateUtils.FORMAT_NO_NOON_MIDNIGHT | DateUtils.FORMAT_SHOW_TIME
544                                 | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE
545                                 | DateUtils.FORMAT_CAP_AMPM);
546             }
547 
548             setTitle(title);
549             titleTextView.setText(title);
550         }
551 
552         TextView textView = findViewById(R.id.message);
553         String messageText = message.getMessageBody();
554         if (textView != null && messageText != null) {
555             int linkMethod = getLinkMethod(message.getSubscriptionId());
556             if (linkMethod != LINK_METHOD_NONE) {
557                 addLinks(textView, messageText, linkMethod);
558             } else {
559                 // Do not add any link to the message text.
560                 textView.setText(messageText);
561             }
562         }
563 
564         String dismissButtonText = getString(R.string.button_dismiss);
565 
566         if (mMessageList.size() > 1) {
567             dismissButtonText += "  (1/" + mMessageList.size() + ")";
568         }
569 
570         ((TextView) findViewById(R.id.dismissButton)).setText(dismissButtonText);
571 
572 
573         setPictogram(context, message);
574     }
575 
576     /**
577      * Set pictogram image
578      * @param context
579      * @param message
580      */
setPictogram(Context context, SmsCbMessage message)581     private void setPictogram(Context context, SmsCbMessage message) {
582         int resId = CellBroadcastResources.getDialogPictogramResource(context, message);
583         ImageView image = findViewById(R.id.pictogramImage);
584         if (resId != -1) {
585             image.setImageResource(resId);
586             image.setVisibility(View.VISIBLE);
587         } else {
588             image.setVisibility(View.GONE);
589         }
590     }
591 
592     /**
593      * Set pictogram to match orientation
594      *
595      * @param orientation The orientation of the pictogram.
596      */
setPictogramAreaLayout(int orientation)597     private void setPictogramAreaLayout(int orientation) {
598         ImageView image = findViewById(R.id.pictogramImage);
599         if (image.getVisibility() == View.VISIBLE) {
600             ViewGroup.LayoutParams params = image.getLayoutParams();
601 
602             if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
603                 Display display = getWindowManager().getDefaultDisplay();
604                 Point point = new Point();
605                 display.getSize(point);
606                 params.width = (int) (point.x * 0.3);
607                 params.height = (int) (point.y * 0.3);
608             } else {
609                 params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
610                 params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
611             }
612 
613             image.setLayoutParams(params);
614         }
615     }
616 
617     /**
618      * Called by {@link CellBroadcastAlertService} to add a new alert to the stack.
619      * @param intent The new intent containing one or more {@link SmsCbMessage}.
620      */
621     @Override
622     @VisibleForTesting
onNewIntent(Intent intent)623     public void onNewIntent(Intent intent) {
624         ArrayList<SmsCbMessage> newMessageList = intent.getParcelableArrayListExtra(
625                 CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA);
626         if (newMessageList != null) {
627             if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) {
628                 mMessageList = newMessageList;
629             } else {
630                 mMessageList.addAll(newMessageList);
631                 if (CellBroadcastSettings.getResources(getApplicationContext(),
632                         SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
633                         .getBoolean(R.bool.show_cmas_messages_in_priority_order)) {
634                     // Sort message list to show messages in a different order than received by
635                     // prioritizing them. Presidential Alert only has top priority.
636                     Collections.sort(
637                             mMessageList,
638                             (Comparator) (o1, o2) -> {
639                                 boolean isPresidentialAlert1 =
640                                         ((SmsCbMessage) o1).isCmasMessage()
641                                                 && ((SmsCbMessage) o1).getCmasWarningInfo()
642                                                 .getMessageClass() == SmsCbCmasInfo
643                                                 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
644                                 boolean isPresidentialAlert2 =
645                                         ((SmsCbMessage) o2).isCmasMessage()
646                                                 && ((SmsCbMessage) o2).getCmasWarningInfo()
647                                                 .getMessageClass() == SmsCbCmasInfo
648                                                 .CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
649                                 if (isPresidentialAlert1 ^ isPresidentialAlert2) {
650                                     return isPresidentialAlert1 ? 1 : -1;
651                                 }
652                                 Long time1 =
653                                         new Long(((SmsCbMessage) o1).getReceivedTime());
654                                 Long time2 =
655                                         new Long(((SmsCbMessage) o2).getReceivedTime());
656                                 return time2.compareTo(time1);
657                             });
658                 }
659             }
660             Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size());
661             hideOptOutDialog(); // Hide opt-out dialog when new alert coming
662             updateAlertText(getLatestMessage());
663             // If the new intent was sent from a notification, dismiss it.
664             clearNotification(intent);
665         } else {
666             Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring");
667         }
668     }
669 
670     /**
671      * Try to cancel any notification that may have started this activity.
672      * @param intent Intent containing extras used to identify if notification needs to be cleared
673      */
clearNotification(Intent intent)674     private void clearNotification(Intent intent) {
675         if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
676             NotificationManager notificationManager =
677                     (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
678             notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
679             CellBroadcastReceiverApp.clearNewMessageList();
680         }
681     }
682 
683     /**
684      * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio}
685      * service if necessary.
686      */
687     @VisibleForTesting
dismiss()688     public void dismiss() {
689         Log.d(TAG, "dismiss");
690         // Stop playing alert sound/vibration/speech (if started)
691         stopService(new Intent(this, CellBroadcastAlertAudio.class));
692 
693         // Cancel any pending alert reminder
694         CellBroadcastAlertReminder.cancelAlertReminder();
695 
696         // Remove the current alert message from the list.
697         SmsCbMessage lastMessage = removeLatestMessage();
698         if (lastMessage == null) {
699             Log.e(TAG, "dismiss() called with empty message list!");
700             finish();
701             return;
702         }
703 
704         // Mark the alert as read.
705         final long deliveryTime = lastMessage.getReceivedTime();
706 
707         // Mark broadcast as read on a background thread.
708         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
709                 .execute((CellBroadcastContentProvider.CellBroadcastOperation) provider
710                         -> provider.markBroadcastRead(Telephony.CellBroadcasts.DELIVERY_TIME,
711                         deliveryTime));
712 
713         // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert).
714         if (lastMessage.isCmasMessage() && lastMessage.getCmasWarningInfo().getMessageClass()
715                 != SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) {
716             mShowOptOutDialog = true;
717         }
718 
719         // If there are older emergency alerts to display, update the alert text and return.
720         SmsCbMessage nextMessage = getLatestMessage();
721         if (nextMessage != null) {
722             updateAlertText(nextMessage);
723             int subId = nextMessage.getSubscriptionId();
724             CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(
725                     getApplicationContext(), subId);
726             if (channelManager.isEmergencyMessage(nextMessage)) {
727                 mAnimationHandler.startIconAnimation(subId);
728             } else {
729                 mAnimationHandler.stopIconAnimation();
730             }
731             return;
732         }
733 
734         // Remove pending screen-off messages (animation messages are removed in onPause()).
735         mScreenOffHandler.stopScreenOnTimer();
736 
737         // Show opt-in/opt-out dialog when the first CMAS alert is received.
738         if (mShowOptOutDialog) {
739             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
740             if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) {
741                 // Clear the flag so the user will only see the opt-out dialog once.
742                 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false)
743                         .apply();
744 
745                 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
746                 if (km.inKeyguardRestrictedInputMode()) {
747                     Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)");
748                     Intent intent = new Intent(this, CellBroadcastOptOutActivity.class);
749                     startActivity(intent);
750                 } else {
751                     Log.d(TAG, "Showing opt-out dialog in current activity");
752                     mOptOutDialog = CellBroadcastOptOutActivity.showOptOutDialog(this);
753                     return; // don't call finish() until user dismisses the dialog
754                 }
755             }
756         }
757         NotificationManager notificationManager =
758                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
759         notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
760         finish();
761     }
762 
763     @Override
onKeyDown(int keyCode, KeyEvent event)764     public boolean onKeyDown(int keyCode, KeyEvent event) {
765         Log.d(TAG, "onKeyDown: " + event);
766         SmsCbMessage message = getLatestMessage();
767         if (CellBroadcastSettings.getResources(getApplicationContext(), message.getSubscriptionId())
768                 .getBoolean(R.bool.mute_by_physical_button)) {
769             switch (event.getKeyCode()) {
770                 // Volume keys and camera keys mute the alert sound/vibration (except ETWS).
771                 case KeyEvent.KEYCODE_VOLUME_UP:
772                 case KeyEvent.KEYCODE_VOLUME_DOWN:
773                 case KeyEvent.KEYCODE_VOLUME_MUTE:
774                 case KeyEvent.KEYCODE_CAMERA:
775                 case KeyEvent.KEYCODE_FOCUS:
776                     // Stop playing alert sound/vibration/speech (if started)
777                     stopService(new Intent(this, CellBroadcastAlertAudio.class));
778                     return true;
779 
780                 default:
781                     break;
782             }
783             return super.onKeyDown(keyCode, event);
784         } else {
785             if (event.getKeyCode() == KeyEvent.KEYCODE_POWER) {
786                 // TODO: do something to prevent screen off
787             }
788             // Disable all physical keys if mute_by_physical_button is false
789             return true;
790         }
791     }
792 
793     @Override
onBackPressed()794     public void onBackPressed() {
795         // Disable back key
796     }
797 
798     /**
799      * Hide opt-out dialog.
800      * In case of any emergency alert invisible, need to hide the opt-out dialog when
801      * new alert coming.
802      */
hideOptOutDialog()803     private void hideOptOutDialog() {
804         if (mOptOutDialog != null && mOptOutDialog.isShowing()) {
805             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
806             prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)
807                     .apply();
808             mOptOutDialog.dismiss();
809         }
810     }
811 
812     /**
813      * Copy the message to clipboard.
814      *
815      * @param message Cell broadcast message.
816      *
817      * @return {@code true} if success, otherwise {@code false};
818      */
819     @VisibleForTesting
copyMessageToClipboard(SmsCbMessage message, Context context)820     public static boolean copyMessageToClipboard(SmsCbMessage message, Context context) {
821         ClipboardManager cm = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE);
822         if (cm == null) return false;
823 
824         cm.setPrimaryClip(ClipData.newPlainText("Alert Message", message.getMessageBody()));
825 
826         String msg = CellBroadcastSettings.getResources(context,
827                 message.getSubscriptionId()).getString(R.string.message_copied);
828         Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
829         return true;
830     }
831 }
832