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     // 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). */
    106         AnimationHandler() {}
    107 
    108         /** Start the warning icon animation. */
    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. */
    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. */
    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. */
    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
    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          */
    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). */
    186         ScreenOffHandler() {}
    187 
    188         /** Add screen on window flags and queue a delayed message to remove them later. */
    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. */
    198         void stopScreenOnTimer() {
    199             removeMessages(mCount.get());
    200             clearWindowFlags();
    201         }
    202 
    203         /** Set the screen on window flags. */
    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. */
    210         private void clearWindowFlags() {
    211             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
    212                     | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    213         }
    214 
    215         @Override
    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. */
    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. */
    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
    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
    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. */
    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
    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. */
    364     protected int getLayoutResId() {
    365         return R.layout.cell_broadcast_alert_fullscreen;
    366     }
    367 
    368     /** Update alert text when a new emergency alert arrives. */
    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
    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
    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      */
    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
    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
    508     public void onBackPressed() {
    509         // ignored
    510     }
    511 }
    512