Home | History | Annotate | Download | only in cellbroadcastreceiver
      1 /*
      2  * Copyright (C) 2016 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.os.PowerManager;
     31 import android.preference.PreferenceManager;
     32 import android.provider.Telephony;
     33 import android.telephony.CellBroadcastMessage;
     34 import android.telephony.SmsCbCmasInfo;
     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  * Custom alert dialog with optional flashing warning icon.
     50  * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}.
     51  */
     52 public class CellBroadcastAlertDialog extends Activity {
     53 
     54     private static final String TAG = "CellBroadcastAlertDialog";
     55 
     56     /** Intent extra for non-emergency alerts sent when user selects the notification. */
     57     static final String FROM_NOTIFICATION_EXTRA = "from_notification";
     58 
     59     // Intent extra to identify if notification was sent while trying to move away from the dialog
     60     //  without acknowledging the dialog
     61     static final String FROM_SAVE_STATE_NOTIFICATION_EXTRA = "from_save_state_notification";
     62 
     63     /** List of cell broadcast messages to display (oldest to newest). */
     64     protected ArrayList<CellBroadcastMessage> mMessageList;
     65 
     66     /** Whether a CMAS alert other than Presidential Alert was displayed. */
     67     private boolean mShowOptOutDialog;
     68 
     69     /** Length of time for the warning icon to be visible. */
     70     private static final int WARNING_ICON_ON_DURATION_MSEC = 800;
     71 
     72     /** Length of time for the warning icon to be off. */
     73     private static final int WARNING_ICON_OFF_DURATION_MSEC = 800;
     74 
     75     /** Length of time to keep the screen turned on. */
     76     private static final int KEEP_SCREEN_ON_DURATION_MSEC = 60000;
     77 
     78     /** Animation handler for the flashing warning icon (emergency alerts only). */
     79     private final AnimationHandler mAnimationHandler = new AnimationHandler();
     80 
     81     /** Handler to add and remove screen on flags for emergency alerts. */
     82     private final ScreenOffHandler mScreenOffHandler = new ScreenOffHandler();
     83 
     84     /**
     85      * Animation handler for the flashing warning icon (emergency alerts only).
     86      */
     87     private class AnimationHandler extends Handler {
     88         /** Latest {@code message.what} value for detecting old messages. */
     89         private final AtomicInteger mCount = new AtomicInteger();
     90 
     91         /** Warning icon state: visible == true, hidden == false. */
     92         private boolean mWarningIconVisible;
     93 
     94         /** The warning icon Drawable. */
     95         private Drawable mWarningIcon;
     96 
     97         /** The View containing the warning icon. */
     98         private ImageView mWarningIconView;
     99 
    100         /** Package local constructor (called from outer class). */
    101         AnimationHandler() {}
    102 
    103         /** Start the warning icon animation. */
    104         void startIconAnimation() {
    105             if (!initDrawableAndImageView()) {
    106                 return;     // init failure
    107             }
    108             mWarningIconVisible = true;
    109             mWarningIconView.setVisibility(View.VISIBLE);
    110             updateIconState();
    111             queueAnimateMessage();
    112         }
    113 
    114         /** Stop the warning icon animation. */
    115         void stopIconAnimation() {
    116             // Increment the counter so the handler will ignore the next message.
    117             mCount.incrementAndGet();
    118             if (mWarningIconView != null) {
    119                 mWarningIconView.setVisibility(View.GONE);
    120             }
    121         }
    122 
    123         /** Update the visibility of the warning icon. */
    124         private void updateIconState() {
    125             mWarningIconView.setImageAlpha(mWarningIconVisible ? 255 : 0);
    126             mWarningIconView.invalidateDrawable(mWarningIcon);
    127         }
    128 
    129         /** Queue a message to animate the warning icon. */
    130         private void queueAnimateMessage() {
    131             int msgWhat = mCount.incrementAndGet();
    132             sendEmptyMessageDelayed(msgWhat, mWarningIconVisible ? WARNING_ICON_ON_DURATION_MSEC
    133                     : WARNING_ICON_OFF_DURATION_MSEC);
    134         }
    135 
    136         @Override
    137         public void handleMessage(Message msg) {
    138             if (msg.what == mCount.get()) {
    139                 mWarningIconVisible = !mWarningIconVisible;
    140                 updateIconState();
    141                 queueAnimateMessage();
    142             }
    143         }
    144 
    145         /**
    146          * Initialize the Drawable and ImageView fields.
    147          * @return true if successful; false if any field failed to initialize
    148          */
    149         private boolean initDrawableAndImageView() {
    150             if (mWarningIcon == null) {
    151                 try {
    152                     mWarningIcon = getResources().getDrawable(R.drawable.ic_warning_googred);
    153                 } catch (Resources.NotFoundException e) {
    154                     Log.e(TAG, "warning icon resource not found", e);
    155                     return false;
    156                 }
    157             }
    158             if (mWarningIconView == null) {
    159                 mWarningIconView = (ImageView) findViewById(R.id.icon);
    160                 if (mWarningIconView != null) {
    161                     mWarningIconView.setImageDrawable(mWarningIcon);
    162                 } else {
    163                     Log.e(TAG, "failed to get ImageView for warning icon");
    164                     return false;
    165                 }
    166             }
    167             return true;
    168         }
    169     }
    170 
    171     /**
    172      * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay,
    173      * remove the flag so the screen can turn off to conserve the battery.
    174      */
    175     private class ScreenOffHandler extends Handler {
    176         /** Latest {@code message.what} value for detecting old messages. */
    177         private final AtomicInteger mCount = new AtomicInteger();
    178 
    179         /** Package local constructor (called from outer class). */
    180         ScreenOffHandler() {}
    181 
    182         /** Add screen on window flags and queue a delayed message to remove them later. */
    183         void startScreenOnTimer() {
    184             addWindowFlags();
    185             int msgWhat = mCount.incrementAndGet();
    186             removeMessages(msgWhat - 1);    // Remove previous message, if any.
    187             sendEmptyMessageDelayed(msgWhat, KEEP_SCREEN_ON_DURATION_MSEC);
    188             Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat);
    189         }
    190 
    191         /** Remove the screen on window flags and any queued screen off message. */
    192         void stopScreenOnTimer() {
    193             removeMessages(mCount.get());
    194             clearWindowFlags();
    195         }
    196 
    197         /** Set the screen on window flags. */
    198         private void addWindowFlags() {
    199             getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
    200                     | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    201         }
    202 
    203         /** Clear the screen on window flags. */
    204         private void clearWindowFlags() {
    205             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
    206                     | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    207         }
    208 
    209         @Override
    210         public void handleMessage(Message msg) {
    211             int msgWhat = msg.what;
    212             if (msgWhat == mCount.get()) {
    213                 clearWindowFlags();
    214                 Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat);
    215             } else {
    216                 Log.e(TAG, "discarding screen off message with id " + msgWhat);
    217             }
    218         }
    219     }
    220 
    221     @Override
    222     protected void onCreate(Bundle savedInstanceState) {
    223         super.onCreate(savedInstanceState);
    224 
    225         final Window win = getWindow();
    226 
    227         // We use a custom title, so remove the standard dialog title bar
    228         win.requestFeature(Window.FEATURE_NO_TITLE);
    229 
    230         // Full screen alerts display above the keyguard and when device is locked.
    231         win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
    232                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
    233                 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
    234 
    235         setFinishOnTouchOutside(false);
    236 
    237         // Initialize the view.
    238         LayoutInflater inflater = LayoutInflater.from(this);
    239         setContentView(inflater.inflate(R.layout.cell_broadcast_alert, null));
    240 
    241         findViewById(R.id.dismissButton).setOnClickListener(
    242                 new Button.OnClickListener() {
    243                     @Override
    244                     public void onClick(View v) {
    245                         dismiss();
    246                     }
    247                 });
    248 
    249         // Get message list from saved Bundle or from Intent.
    250         if (savedInstanceState != null) {
    251             Log.d(TAG, "onCreate getting message list from saved instance state");
    252             mMessageList = savedInstanceState.getParcelableArrayList(
    253                     CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA);
    254         } else {
    255             Log.d(TAG, "onCreate getting message list from intent");
    256             Intent intent = getIntent();
    257             mMessageList = intent.getParcelableArrayListExtra(
    258                     CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA);
    259 
    260             // If we were started from a notification, dismiss it.
    261             clearNotification(intent);
    262         }
    263 
    264         if (mMessageList == null || mMessageList.size() == 0) {
    265             Log.e(TAG, "onCreate failed as message list is null or empty");
    266             finish();
    267         } else {
    268             Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size());
    269         }
    270 
    271         // For emergency alerts, keep screen on so the user can read it
    272         CellBroadcastMessage message = getLatestMessage();
    273         if (message != null && CellBroadcastAlertService.
    274                 isEmergencyMessage(this, message)) {
    275             Log.d(TAG, "onCreate setting screen on timer for emergency alert");
    276             mScreenOffHandler.startScreenOnTimer();
    277         }
    278 
    279         updateAlertText(message);
    280     }
    281 
    282     /**
    283      * Start animating warning icon.
    284      */
    285     @Override
    286     protected void onResume() {
    287         super.onResume();
    288         CellBroadcastMessage message = getLatestMessage();
    289         if (message != null && CellBroadcastAlertService.
    290                 isEmergencyMessage(this, message)) {
    291             mAnimationHandler.startIconAnimation();
    292         }
    293     }
    294 
    295     /**
    296      * Stop animating warning icon.
    297      */
    298     @Override
    299     protected void onPause() {
    300         Log.d(TAG, "onPause called");
    301         mAnimationHandler.stopIconAnimation();
    302         super.onPause();
    303     }
    304 
    305     @Override
    306     protected void onStop() {
    307         super.onStop();
    308         // When the activity goes in background eg. clicking Home button, send notification.
    309         // Avoid doing this when activity will be recreated because of orientation change or if
    310         // screen goes off
    311         PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
    312         if (!(isChangingConfigurations() || getLatestMessage() == null) && pm.isScreenOn()) {
    313             CellBroadcastAlertService.addToNotificationBar(getLatestMessage(), mMessageList,
    314                     getApplicationContext(), true);
    315         }
    316     }
    317 
    318     /** Returns the currently displayed message. */
    319     CellBroadcastMessage getLatestMessage() {
    320         int index = mMessageList.size() - 1;
    321         if (index >= 0) {
    322             return mMessageList.get(index);
    323         } else {
    324             Log.d(TAG, "getLatestMessage returns null");
    325             return null;
    326         }
    327     }
    328 
    329     /** Removes and returns the currently displayed message. */
    330     private CellBroadcastMessage removeLatestMessage() {
    331         int index = mMessageList.size() - 1;
    332         if (index >= 0) {
    333             return mMessageList.remove(index);
    334         } else {
    335             return null;
    336         }
    337     }
    338 
    339     /**
    340      * Save the list of messages so the state can be restored later.
    341      * @param outState Bundle in which to place the saved state.
    342      */
    343     @Override
    344     protected void onSaveInstanceState(Bundle outState) {
    345         super.onSaveInstanceState(outState);
    346         outState.putParcelableArrayList(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessageList);
    347     }
    348 
    349     /**
    350      * Update alert text when a new emergency alert arrives.
    351      * @param message CB message which is used to update alert text.
    352      */
    353     private void updateAlertText(CellBroadcastMessage message) {
    354         int titleId = CellBroadcastResources.getDialogTitleResource(
    355                 getApplicationContext(), message);
    356 
    357         String title = getText(titleId).toString();
    358         TextView titleTextView = findViewById(R.id.alertTitle);
    359 
    360         if (getApplicationContext().getResources().getBoolean(R.bool.show_date_time_title)) {
    361             titleTextView.setSingleLine(false);
    362             title += "\n" + message.getDateString(getApplicationContext());
    363         }
    364 
    365         setTitle(title);
    366         titleTextView.setText(title);
    367 
    368         ((TextView) findViewById(R.id.message)).setText(message.getMessageBody());
    369 
    370         String dismissButtonText = getString(R.string.button_dismiss);
    371 
    372         if (mMessageList.size() > 1) {
    373             dismissButtonText += "  (1/" + mMessageList.size() + ")";
    374         }
    375 
    376         ((TextView) findViewById(R.id.dismissButton)).setText(dismissButtonText);
    377     }
    378 
    379     /**
    380      * Called by {@link CellBroadcastAlertService} to add a new alert to the stack.
    381      * @param intent The new intent containing one or more {@link CellBroadcastMessage}s.
    382      */
    383     @Override
    384     protected void onNewIntent(Intent intent) {
    385         ArrayList<CellBroadcastMessage> newMessageList = intent.getParcelableArrayListExtra(
    386                 CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA);
    387         if (newMessageList != null) {
    388             if (intent.getBooleanExtra(FROM_SAVE_STATE_NOTIFICATION_EXTRA, false)) {
    389                 mMessageList = newMessageList;
    390             } else {
    391                 mMessageList.addAll(newMessageList);
    392             }
    393             Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size());
    394             updateAlertText(getLatestMessage());
    395             // If the new intent was sent from a notification, dismiss it.
    396             clearNotification(intent);
    397         } else {
    398             Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring");
    399         }
    400     }
    401 
    402     /**
    403      * Try to cancel any notification that may have started this activity.
    404      * @param intent Intent containing extras used to identify if notification needs to be cleared
    405      */
    406     private void clearNotification(Intent intent) {
    407         if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
    408             NotificationManager notificationManager =
    409                     (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    410             notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
    411             CellBroadcastReceiverApp.clearNewMessageList();
    412         }
    413     }
    414 
    415     /**
    416      * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio}
    417      * service if necessary.
    418      */
    419     void dismiss() {
    420         Log.d(TAG, "dismiss");
    421         // Stop playing alert sound/vibration/speech (if started)
    422         stopService(new Intent(this, CellBroadcastAlertAudio.class));
    423 
    424         // Cancel any pending alert reminder
    425         CellBroadcastAlertReminder.cancelAlertReminder();
    426 
    427         // Remove the current alert message from the list.
    428         CellBroadcastMessage lastMessage = removeLatestMessage();
    429         if (lastMessage == null) {
    430             Log.e(TAG, "dismiss() called with empty message list!");
    431             finish();
    432             return;
    433         }
    434 
    435         // Mark the alert as read.
    436         final long deliveryTime = lastMessage.getDeliveryTime();
    437 
    438         // Mark broadcast as read on a background thread.
    439         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
    440                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
    441                     @Override
    442                     public boolean execute(CellBroadcastContentProvider provider) {
    443                         return provider.markBroadcastRead(
    444                                 Telephony.CellBroadcasts.DELIVERY_TIME, deliveryTime);
    445                     }
    446                 });
    447 
    448         // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert).
    449         if (lastMessage.isCmasMessage() && lastMessage.getCmasMessageClass() !=
    450                 SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) {
    451             mShowOptOutDialog = true;
    452         }
    453 
    454         // If there are older emergency alerts to display, update the alert text and return.
    455         CellBroadcastMessage nextMessage = getLatestMessage();
    456         if (nextMessage != null) {
    457             updateAlertText(nextMessage);
    458             if (CellBroadcastAlertService.isEmergencyMessage(
    459                     this, nextMessage)) {
    460                 mAnimationHandler.startIconAnimation();
    461             } else {
    462                 mAnimationHandler.stopIconAnimation();
    463             }
    464             return;
    465         }
    466 
    467         // Remove pending screen-off messages (animation messages are removed in onPause()).
    468         mScreenOffHandler.stopScreenOnTimer();
    469 
    470         // Show opt-in/opt-out dialog when the first CMAS alert is received.
    471         if (mShowOptOutDialog) {
    472             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    473             if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) {
    474                 // Clear the flag so the user will only see the opt-out dialog once.
    475                 prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false)
    476                         .apply();
    477 
    478                 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
    479                 if (km.inKeyguardRestrictedInputMode()) {
    480                     Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)");
    481                     Intent intent = new Intent(this, CellBroadcastOptOutActivity.class);
    482                     startActivity(intent);
    483                 } else {
    484                     Log.d(TAG, "Showing opt-out dialog in current activity");
    485                     CellBroadcastOptOutActivity.showOptOutDialog(this);
    486                     return; // don't call finish() until user dismisses the dialog
    487                 }
    488             }
    489         }
    490         NotificationManager notificationManager =
    491                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    492         notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
    493         finish();
    494     }
    495 
    496     @Override
    497     public boolean dispatchKeyEvent(KeyEvent event) {
    498         CellBroadcastMessage message = getLatestMessage();
    499         if (message != null && !message.isEtwsMessage()) {
    500             switch (event.getKeyCode()) {
    501                 // Volume keys and camera keys mute the alert sound/vibration (except ETWS).
    502                 case KeyEvent.KEYCODE_VOLUME_UP:
    503                 case KeyEvent.KEYCODE_VOLUME_DOWN:
    504                 case KeyEvent.KEYCODE_VOLUME_MUTE:
    505                 case KeyEvent.KEYCODE_CAMERA:
    506                 case KeyEvent.KEYCODE_FOCUS:
    507                     // Stop playing alert sound/vibration/speech (if started)
    508                     stopService(new Intent(this, CellBroadcastAlertAudio.class));
    509                     return true;
    510 
    511                 default:
    512                     break;
    513             }
    514         }
    515         return super.dispatchKeyEvent(event);
    516     }
    517 
    518     @Override
    519     public void onBackPressed() {
    520         // Disable back key
    521     }
    522 }
    523