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