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
17package com.android.cellbroadcastreceiver;
18
19import android.app.Activity;
20import android.app.KeyguardManager;
21import android.app.NotificationManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.content.res.Resources;
26import android.graphics.drawable.Drawable;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.Message;
30import android.preference.PreferenceManager;
31import android.provider.Telephony;
32import android.telephony.CellBroadcastMessage;
33import android.telephony.SmsCbCmasInfo;
34import android.util.Log;
35import android.view.KeyEvent;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.Window;
39import android.view.WindowManager;
40import android.widget.Button;
41import android.widget.ImageView;
42import android.widget.TextView;
43
44import java.util.ArrayList;
45import 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 */
52public 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    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). */
102        AnimationHandler() {}
103
104        /** Start the warning icon animation. */
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. */
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. */
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. */
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
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         */
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). */
182        ScreenOffHandler() {}
183
184        /** Add screen on window flags and queue a delayed message to remove them later. */
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. */
194        void stopScreenOnTimer() {
195            removeMessages(mCount.get());
196            clearWindowFlags();
197        }
198
199        /** Set the screen on window flags. */
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. */
206        private void clearWindowFlags() {
207            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
208                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
209        }
210
211        @Override
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. */
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. */
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
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
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. */
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
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. */
346    protected int getLayoutResId() {
347        return R.layout.cell_broadcast_alert_fullscreen;
348    }
349
350    /** Update alert text when a new emergency alert arrives. */
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
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
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     */
388    void dismiss() {
389        // Stop playing alert sound/vibration/speech (if started)
390        stopService(new Intent(this, CellBroadcastAlertAudio.class));
391
392        // Cancel any pending alert reminder
393        CellBroadcastAlertReminder.cancelAlertReminder();
394
395        // Remove the current alert message from the list.
396        CellBroadcastMessage lastMessage = removeLatestMessage();
397        if (lastMessage == null) {
398            Log.e(TAG, "dismiss() called with empty message list!");
399            return;
400        }
401
402        // Mark the alert as read.
403        final long deliveryTime = lastMessage.getDeliveryTime();
404
405        // Mark broadcast as read on a background thread.
406        new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
407                .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
408                    @Override
409                    public boolean execute(CellBroadcastContentProvider provider) {
410                        return provider.markBroadcastRead(
411                                Telephony.CellBroadcasts.DELIVERY_TIME, deliveryTime);
412                    }
413                });
414
415        // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert).
416        if (lastMessage.isCmasMessage() && lastMessage.getCmasMessageClass() !=
417                SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) {
418            mShowOptOutDialog = true;
419        }
420
421        // If there are older emergency alerts to display, update the alert text and return.
422        CellBroadcastMessage nextMessage = getLatestMessage();
423        if (nextMessage != null) {
424            updateAlertText(nextMessage);
425            if (CellBroadcastConfigService.isEmergencyAlertMessage(nextMessage)) {
426                mAnimationHandler.startIconAnimation();
427            } else {
428                mAnimationHandler.stopIconAnimation();
429            }
430            return;
431        }
432
433        // Remove pending screen-off messages (animation messages are removed in onPause()).
434        mScreenOffHandler.stopScreenOnTimer();
435
436        // Show opt-in/opt-out dialog when the first CMAS alert is received.
437        if (mShowOptOutDialog) {
438            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
439            if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) {
440                // Clear the flag so the user will only see the opt-out dialog once.
441                prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false)
442                        .apply();
443
444                KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
445                if (km.inKeyguardRestrictedInputMode()) {
446                    Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)");
447                    Intent intent = new Intent(this, CellBroadcastOptOutActivity.class);
448                    startActivity(intent);
449                } else {
450                    Log.d(TAG, "Showing opt-out dialog in current activity");
451                    CellBroadcastOptOutActivity.showOptOutDialog(this);
452                    return; // don't call finish() until user dismisses the dialog
453                }
454            }
455        }
456
457        finish();
458    }
459
460    @Override
461    public boolean dispatchKeyEvent(KeyEvent event) {
462        CellBroadcastMessage message = getLatestMessage();
463        if (message != null && !message.isEtwsMessage()) {
464            switch (event.getKeyCode()) {
465                // Volume keys and camera keys mute the alert sound/vibration (except ETWS).
466                case KeyEvent.KEYCODE_VOLUME_UP:
467                case KeyEvent.KEYCODE_VOLUME_DOWN:
468                case KeyEvent.KEYCODE_VOLUME_MUTE:
469                case KeyEvent.KEYCODE_CAMERA:
470                case KeyEvent.KEYCODE_FOCUS:
471                    // Stop playing alert sound/vibration/speech (if started)
472                    stopService(new Intent(this, CellBroadcastAlertAudio.class));
473                    return true;
474
475                default:
476                    break;
477            }
478        }
479        return super.dispatchKeyEvent(event);
480    }
481
482    /**
483     * Ignore the back button for emergency alerts (overridden by alert dialog so that the dialog
484     * is dismissed).
485     */
486    @Override
487    public void onBackPressed() {
488        // ignored
489    }
490}
491