Home | History | Annotate | Download | only in cellbroadcastreceiver
      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). */
    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         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
    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
    489     public void onBackPressed() {
    490         // ignored
    491     }
    492 }
    493