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.telephony.SubscriptionManager;
35import android.util.Log;
36import android.view.KeyEvent;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.Window;
40import android.view.WindowManager;
41import android.widget.Button;
42import android.widget.ImageView;
43import android.widget.TextView;
44
45import java.util.ArrayList;
46import 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 */
53public 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). */
103        AnimationHandler() {}
104
105        /** Start the warning icon animation. */
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. */
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. */
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. */
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
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         */
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). */
183        ScreenOffHandler() {}
184
185        /** Add screen on window flags and queue a delayed message to remove them later. */
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. */
195        void stopScreenOnTimer() {
196            removeMessages(mCount.get());
197            clearWindowFlags();
198        }
199
200        /** Set the screen on window flags. */
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. */
207        private void clearWindowFlags() {
208            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
209                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
210        }
211
212        @Override
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. */
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. */
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
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
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. */
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
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. */
347    protected int getLayoutResId() {
348        return R.layout.cell_broadcast_alert_fullscreen;
349    }
350
351    /** Update alert text when a new emergency alert arrives. */
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
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
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     */
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
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
490    public void onBackPressed() {
491        // ignored
492    }
493}
494