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