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