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