• 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.app.Activity;
20 import android.app.KeyguardManager;
21 import android.app.NotificationManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.SharedPreferences;
25 import android.content.res.Resources;
26 import android.graphics.drawable.Drawable;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.os.PowerManager;
31 import android.preference.PreferenceManager;
32 import android.provider.Telephony;
33 import android.telephony.CellBroadcastMessage;
34 import android.telephony.SmsCbCmasInfo;
35 import android.util.Log;
36 import android.view.KeyEvent;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.Window;
40 import android.view.WindowManager;
41 import android.widget.Button;
42 import android.widget.ImageView;
43 import android.widget.TextView;
44 
45 import java.util.ArrayList;
46 import java.util.concurrent.atomic.AtomicInteger;
47 
48 /**
49  * Custom alert dialog with optional flashing warning icon.
50  * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}.
51  */
52 public class CellBroadcastAlertDialog extends Activity {
53 
54     private static final String TAG = "CellBroadcastAlertDialog";
55 
56     /** Intent extra for non-emergency alerts sent when user selects the notification. */
57     static final String FROM_NOTIFICATION_EXTRA = "from_notification";
58 
59     // Intent extra to identify if notification was sent while trying to move away from the dialog
60     //  without acknowledging the dialog
61     static final String FROM_SAVE_STATE_NOTIFICATION_EXTRA = "from_save_state_notification";
62 
63     /** List of cell broadcast messages to display (oldest to newest). */
64     protected ArrayList<CellBroadcastMessage> mMessageList;
65 
66     /** Whether a CMAS alert other than Presidential Alert was displayed. */
67     private boolean mShowOptOutDialog;
68 
69     /** Length of time for the warning icon to be visible. */
70     private static final int WARNING_ICON_ON_DURATION_MSEC = 800;
71 
72     /** Length of time for the warning icon to be off. */
73     private static final int WARNING_ICON_OFF_DURATION_MSEC = 800;
74 
75     /** Length of time to keep the screen turned on. */
76     private static final int KEEP_SCREEN_ON_DURATION_MSEC = 60000;
77 
78     /** Animation handler for the flashing warning icon (emergency alerts only). */
79     private final AnimationHandler mAnimationHandler = new AnimationHandler();
80 
81     /** Handler to add and remove screen on flags for emergency alerts. */
82     private final ScreenOffHandler mScreenOffHandler = new ScreenOffHandler();
83 
84     /**
85      * Animation handler for the flashing warning icon (emergency alerts only).
86      */
87     private class AnimationHandler extends Handler {
88         /** Latest {@code message.what} value for detecting old messages. */
89         private final AtomicInteger mCount = new AtomicInteger();
90 
91         /** Warning icon state: visible == true, hidden == false. */
92         private boolean mWarningIconVisible;
93 
94         /** The warning icon Drawable. */
95         private Drawable mWarningIcon;
96 
97         /** The View containing the warning icon. */
98         private ImageView mWarningIconView;
99 
100         /** Package local constructor (called from outer class). */
AnimationHandler()101         AnimationHandler() {}
102 
103         /** Start the warning icon animation. */
startIconAnimation()104         void startIconAnimation() {
105             if (!initDrawableAndImageView()) {
106                 return;     // init failure
107             }
108             mWarningIconVisible = true;
109             mWarningIconView.setVisibility(View.VISIBLE);
110             updateIconState();
111             queueAnimateMessage();
112         }
113 
114         /** Stop the warning icon animation. */
stopIconAnimation()115         void stopIconAnimation() {
116             // Increment the counter so the handler will ignore the next message.
117             mCount.incrementAndGet();
118             if (mWarningIconView != null) {
119                 mWarningIconView.setVisibility(View.GONE);
120             }
121         }
122 
123         /** Update the visibility of the warning icon. */
updateIconState()124         private void updateIconState() {
125             mWarningIconView.setImageAlpha(mWarningIconVisible ? 255 : 0);
126             mWarningIconView.invalidateDrawable(mWarningIcon);
127         }
128 
129         /** Queue a message to animate the warning icon. */
queueAnimateMessage()130         private void queueAnimateMessage() {
131             int msgWhat = mCount.incrementAndGet();
132             sendEmptyMessageDelayed(msgWhat, mWarningIconVisible ? WARNING_ICON_ON_DURATION_MSEC
133                     : WARNING_ICON_OFF_DURATION_MSEC);
134         }
135 
136         @Override
handleMessage(Message msg)137         public void handleMessage(Message msg) {
138             if (msg.what == mCount.get()) {
139                 mWarningIconVisible = !mWarningIconVisible;
140                 updateIconState();
141                 queueAnimateMessage();
142             }
143         }
144 
145         /**
146          * Initialize the Drawable and ImageView fields.
147          * @return true if successful; false if any field failed to initialize
148          */
initDrawableAndImageView()149         private boolean initDrawableAndImageView() {
150             if (mWarningIcon == null) {
151                 try {
152                     mWarningIcon = CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(
153                             getApplicationContext()).getDrawable(R.drawable.ic_warning_googred);
154                 } catch (Resources.NotFoundException e) {
155                     Log.e(TAG, "warning icon resource not found", e);
156                     return false;
157                 }
158             }
159             if (mWarningIconView == null) {
160                 mWarningIconView = (ImageView) findViewById(R.id.icon);
161                 if (mWarningIconView != null) {
162                     mWarningIconView.setImageDrawable(mWarningIcon);
163                 } else {
164                     Log.e(TAG, "failed to get ImageView for warning icon");
165                     return false;
166                 }
167             }
168             return true;
169         }
170     }
171 
172     /**
173      * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay,
174      * remove the flag so the screen can turn off to conserve the battery.
175      */
176     private class ScreenOffHandler extends Handler {
177         /** Latest {@code message.what} value for detecting old messages. */
178         private final AtomicInteger mCount = new AtomicInteger();
179 
180         /** Package local constructor (called from outer class). */
ScreenOffHandler()181         ScreenOffHandler() {}
182 
183         /** Add screen on window flags and queue a delayed message to remove them later. */
startScreenOnTimer()184         void startScreenOnTimer() {
185             addWindowFlags();
186             int msgWhat = mCount.incrementAndGet();
187             removeMessages(msgWhat - 1);    // Remove previous message, if any.
188             sendEmptyMessageDelayed(msgWhat, KEEP_SCREEN_ON_DURATION_MSEC);
189             Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat);
190         }
191 
192         /** Remove the screen on window flags and any queued screen off message. */
stopScreenOnTimer()193         void stopScreenOnTimer() {
194             removeMessages(mCount.get());
195             clearWindowFlags();
196         }
197 
198         /** Set the screen on window flags. */
addWindowFlags()199         private void addWindowFlags() {
200             getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
201                     | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
202         }
203 
204         /** Clear the screen on window flags. */
clearWindowFlags()205         private void clearWindowFlags() {
206             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
207                     | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
208         }
209 
210         @Override
handleMessage(Message msg)211         public void handleMessage(Message msg) {
212             int msgWhat = msg.what;
213             if (msgWhat == mCount.get()) {
214                 clearWindowFlags();
215                 Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat);
216             } else {
217                 Log.e(TAG, "discarding screen off message with id " + msgWhat);
218             }
219         }
220     }
221 
222     @Override
onCreate(Bundle savedInstanceState)223     protected void onCreate(Bundle savedInstanceState) {
224         super.onCreate(savedInstanceState);
225 
226         final Window win = getWindow();
227 
228         // We use a custom title, so remove the standard dialog title bar
229         win.requestFeature(Window.FEATURE_NO_TITLE);
230 
231         // Full screen alerts display above the keyguard and when device is locked.
232         win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
233                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
234                 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
235 
236         setFinishOnTouchOutside(false);
237 
238         // Initialize the view.
239         LayoutInflater inflater = LayoutInflater.from(this);
240         setContentView(inflater.inflate(R.layout.cell_broadcast_alert, null));
241 
242         findViewById(R.id.dismissButton).setOnClickListener(
243                 new Button.OnClickListener() {
244                     @Override
245                     public void onClick(View v) {
246                         dismiss();
247                     }
248                 });
249 
250         // Get message list from saved Bundle or from Intent.
251         if (savedInstanceState != null) {
252             Log.d(TAG, "onCreate getting message list from saved instance state");
253             mMessageList = savedInstanceState.getParcelableArrayList(
254                     CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA);
255         } else {
256             Log.d(TAG, "onCreate getting message list from intent");
257             Intent intent = getIntent();
258             mMessageList = intent.getParcelableArrayListExtra(
259                     CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA);
260 
261             // If we were started from a notification, dismiss it.
262             clearNotification(intent);
263         }
264 
265         if (mMessageList == null || mMessageList.size() == 0) {
266             Log.e(TAG, "onCreate failed as message list is null or empty");
267             finish();
268         } else {
269             Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size());
270         }
271 
272         // For emergency alerts, keep screen on so the user can read it
273         CellBroadcastMessage message = getLatestMessage();
274         if (message != null && CellBroadcastChannelManager.isEmergencyMessage(
275                 this, message)) {
276             Log.d(TAG, "onCreate setting screen on timer for emergency alert");
277             mScreenOffHandler.startScreenOnTimer();
278         }
279 
280         updateAlertText(message);
281     }
282 
283     /**
284      * Start animating warning icon.
285      */
286     @Override
onResume()287     protected void onResume() {
288         super.onResume();
289         CellBroadcastMessage message = getLatestMessage();
290         if (message != null && CellBroadcastChannelManager.isEmergencyMessage(this, message)) {
291             mAnimationHandler.startIconAnimation();
292         }
293     }
294 
295     /**
296      * Stop animating warning icon.
297      */
298     @Override
onPause()299     protected void onPause() {
300         Log.d(TAG, "onPause called");
301         mAnimationHandler.stopIconAnimation();
302         super.onPause();
303     }
304 
305     @Override
onStop()306     protected void onStop() {
307         super.onStop();
308         // When the activity goes in background eg. clicking Home button, send notification.
309         // Avoid doing this when activity will be recreated because of orientation change or if
310         // screen goes off
311         PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
312         if (!(isChangingConfigurations() || getLatestMessage() == null) && pm.isScreenOn()) {
313             CellBroadcastAlertService.addToNotificationBar(getLatestMessage(), mMessageList,
314                     getApplicationContext(), true);
315         }
316     }
317 
318     /** Returns the currently displayed message. */
getLatestMessage()319     CellBroadcastMessage getLatestMessage() {
320         int index = mMessageList.size() - 1;
321         if (index >= 0) {
322             return mMessageList.get(index);
323         } else {
324             Log.d(TAG, "getLatestMessage returns null");
325             return null;
326         }
327     }
328 
329     /** Removes and returns the currently displayed message. */
removeLatestMessage()330     private CellBroadcastMessage removeLatestMessage() {
331         int index = mMessageList.size() - 1;
332         if (index >= 0) {
333             return mMessageList.remove(index);
334         } else {
335             return null;
336         }
337     }
338 
339     /**
340      * Save the list of messages so the state can be restored later.
341      * @param outState Bundle in which to place the saved state.
342      */
343     @Override
onSaveInstanceState(Bundle outState)344     protected void onSaveInstanceState(Bundle outState) {
345         super.onSaveInstanceState(outState);
346         outState.putParcelableArrayList(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessageList);
347     }
348 
349     /**
350      * Update alert text when a new emergency alert arrives.
351      * @param message CB message which is used to update alert text.
352      */
updateAlertText(CellBroadcastMessage message)353     private void updateAlertText(CellBroadcastMessage message) {
354         Context context = getApplicationContext();
355         int titleId = CellBroadcastResources.getDialogTitleResource(context, message);
356 
357         String title = getText(titleId).toString();
358         TextView titleTextView = findViewById(R.id.alertTitle);
359 
360         if (CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context)
361                 .getBoolean(R.bool.show_date_time_title)) {
362             titleTextView.setSingleLine(false);
363             title += "\n" + message.getDateString(context);
364         }
365 
366         setTitle(title);
367         titleTextView.setText(title);
368 
369         ((TextView) findViewById(R.id.message)).setText(message.getMessageBody());
370 
371         String dismissButtonText = getString(R.string.button_dismiss);
372 
373         if (mMessageList.size() > 1) {
374             dismissButtonText += "  (1/" + mMessageList.size() + ")";
375         }
376 
377         ((TextView) findViewById(R.id.dismissButton)).setText(dismissButtonText);
378     }
379 
380     /**
381      * Called by {@link CellBroadcastAlertService} to add a new alert to the stack.
382      * @param intent The new intent containing one or more {@link CellBroadcastMessage}s.
383      */
384     @Override
onNewIntent(Intent intent)385     protected void onNewIntent(Intent intent) {
386         ArrayList<CellBroadcastMessage> newMessageList = intent.getParcelableArrayListExtra(
387                 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA);
388         if (newMessageList != null) {
389             if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) {
390                 mMessageList = newMessageList;
391             } else {
392                 mMessageList.addAll(newMessageList);
393             }
394             Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size());
395             updateAlertText(getLatestMessage());
396             // If the new intent was sent from a notification, dismiss it.
397             clearNotification(intent);
398         } else {
399             Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring");
400         }
401     }
402 
403     /**
404      * Try to cancel any notification that may have started this activity.
405      * @param intent Intent containing extras used to identify if notification needs to be cleared
406      */
clearNotification(Intent intent)407     private void clearNotification(Intent intent) {
408         if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
409             NotificationManager notificationManager =
410                     (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
411             notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
412             CellBroadcastReceiverApp.clearNewMessageList();
413         }
414     }
415 
416     /**
417      * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio}
418      * service if necessary.
419      */
dismiss()420     void dismiss() {
421         Log.d(TAG, "dismiss");
422         // Stop playing alert sound/vibration/speech (if started)
423         stopService(new Intent(this, CellBroadcastAlertAudio.class));
424 
425         // Cancel any pending alert reminder
426         CellBroadcastAlertReminder.cancelAlertReminder();
427 
428         // Remove the current alert message from the list.
429         CellBroadcastMessage lastMessage = removeLatestMessage();
430         if (lastMessage == null) {
431             Log.e(TAG, "dismiss() called with empty message list!");
432             finish();
433             return;
434         }
435 
436         // Mark the alert as read.
437         final long deliveryTime = lastMessage.getDeliveryTime();
438 
439         // Mark broadcast as read on a background thread.
440         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
441                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
442                     @Override
443                     public boolean execute(CellBroadcastContentProvider provider) {
444                         return provider.markBroadcastRead(
445                                 Telephony.CellBroadcasts.DELIVERY_TIME, deliveryTime);
446                     }
447                 });
448 
449         // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert).
450         if (lastMessage.isCmasMessage() && lastMessage.getCmasMessageClass() !=
451                 SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) {
452             mShowOptOutDialog = true;
453         }
454 
455         // If there are older emergency alerts to display, update the alert text and return.
456         CellBroadcastMessage nextMessage = getLatestMessage();
457         if (nextMessage != null) {
458             updateAlertText(nextMessage);
459             if (CellBroadcastChannelManager.isEmergencyMessage(
460                     this, nextMessage)) {
461                 mAnimationHandler.startIconAnimation();
462             } else {
463                 mAnimationHandler.stopIconAnimation();
464             }
465             return;
466         }
467 
468         // Remove pending screen-off messages (animation messages are removed in onPause()).
469         mScreenOffHandler.stopScreenOnTimer();
470 
471         // Show opt-in/opt-out dialog when the first CMAS alert is received.
472         if (mShowOptOutDialog) {
473             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
474             if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) {
475                 // Clear the flag so the user will only see the opt-out dialog once.
476                 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false)
477                         .apply();
478 
479                 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
480                 if (km.inKeyguardRestrictedInputMode()) {
481                     Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)");
482                     Intent intent = new Intent(this, CellBroadcastOptOutActivity.class);
483                     startActivity(intent);
484                 } else {
485                     Log.d(TAG, "Showing opt-out dialog in current activity");
486                     CellBroadcastOptOutActivity.showOptOutDialog(this);
487                     return; // don't call finish() until user dismisses the dialog
488                 }
489             }
490         }
491         NotificationManager notificationManager =
492                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
493         notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
494         finish();
495     }
496 
497     @Override
dispatchKeyEvent(KeyEvent event)498     public boolean dispatchKeyEvent(KeyEvent event) {
499         CellBroadcastMessage message = getLatestMessage();
500         if (message != null && !message.isEtwsMessage()) {
501             switch (event.getKeyCode()) {
502                 // Volume keys and camera keys mute the alert sound/vibration (except ETWS).
503                 case KeyEvent.KEYCODE_VOLUME_UP:
504                 case KeyEvent.KEYCODE_VOLUME_DOWN:
505                 case KeyEvent.KEYCODE_VOLUME_MUTE:
506                 case KeyEvent.KEYCODE_CAMERA:
507                 case KeyEvent.KEYCODE_FOCUS:
508                     // Stop playing alert sound/vibration/speech (if started)
509                     stopService(new Intent(this, CellBroadcastAlertAudio.class));
510                     return true;
511 
512                 default:
513                     break;
514             }
515         }
516         return super.dispatchKeyEvent(event);
517     }
518 
519     @Override
onBackPressed()520     public void onBackPressed() {
521         // Disable back key
522     }
523 }
524