Home | History | Annotate | Download | only in cellbroadcastreceiver
      1 /*
      2  * Copyright (C) 2011 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.KeyguardManager;
     20 import android.app.Notification;
     21 import android.app.NotificationManager;
     22 import android.app.PendingIntent;
     23 import android.app.Service;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.SharedPreferences;
     27 import android.os.Bundle;
     28 import android.os.IBinder;
     29 import android.os.PowerManager;
     30 import android.preference.PreferenceManager;
     31 import android.provider.Telephony;
     32 import android.telephony.CellBroadcastMessage;
     33 import android.telephony.SmsCbCmasInfo;
     34 import android.telephony.SmsCbLocation;
     35 import android.telephony.SmsCbMessage;
     36 import android.util.Log;
     37 
     38 import java.util.HashSet;
     39 
     40 /**
     41  * This service manages the display and animation of broadcast messages.
     42  * Emergency messages display with a flashing animated exclamation mark icon,
     43  * and an alert tone is played when the alert is first shown to the user
     44  * (but not when the user views a previously received broadcast).
     45  */
     46 public class CellBroadcastAlertService extends Service {
     47     private static final String TAG = "CellBroadcastAlertService";
     48 
     49     /** Identifier for notification ID extra. */
     50     public static final String SMS_CB_NOTIFICATION_ID_EXTRA =
     51             "com.android.cellbroadcastreceiver.SMS_CB_NOTIFICATION_ID";
     52 
     53     /** Intent extra to indicate a previously unread alert. */
     54     static final String NEW_ALERT_EXTRA = "com.android.cellbroadcastreceiver.NEW_ALERT";
     55 
     56     /** Intent action to display alert dialog/notification, after verifying the alert is new. */
     57     static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
     58 
     59     /** Use the same notification ID for non-emergency alerts. */
     60     static final int NOTIFICATION_ID = 1;
     61 
     62     /** CPU wake lock while handling emergency alert notification. */
     63     private PowerManager.WakeLock mWakeLock;
     64 
     65     /** Hold the wake lock for 5 seconds, which should be enough time to display the alert. */
     66     private static final int WAKE_LOCK_TIMEOUT = 5000;
     67 
     68     /** Container for message ID and geographical scope, for duplicate message detection. */
     69     private static final class MessageIdAndScope {
     70         private final int mMessageId;
     71         private final SmsCbLocation mLocation;
     72 
     73         MessageIdAndScope(int messageId, SmsCbLocation location) {
     74             mMessageId = messageId;
     75             mLocation = location;
     76         }
     77 
     78         @Override
     79         public int hashCode() {
     80             return mMessageId * 31 + mLocation.hashCode();
     81         }
     82 
     83         @Override
     84         public boolean equals(Object o) {
     85             if (o == this) {
     86                 return true;
     87             }
     88             if (o instanceof MessageIdAndScope) {
     89                 MessageIdAndScope other = (MessageIdAndScope) o;
     90                 return (mMessageId == other.mMessageId && mLocation.equals(other.mLocation));
     91             }
     92             return false;
     93         }
     94 
     95         @Override
     96         public String toString() {
     97             return "{messageId: " + mMessageId + " location: " + mLocation.toString() + '}';
     98         }
     99     }
    100 
    101     /** Cache of received message IDs, for duplicate message detection. */
    102     private static final HashSet<MessageIdAndScope> sCmasIdList = new HashSet<MessageIdAndScope>(8);
    103 
    104     @Override
    105     public int onStartCommand(Intent intent, int flags, int startId) {
    106         String action = intent.getAction();
    107         if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) ||
    108                 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
    109             handleCellBroadcastIntent(intent);
    110         } else if (SHOW_NEW_ALERT_ACTION.equals(action)) {
    111             showNewAlert(intent);
    112         } else {
    113             Log.e(TAG, "Unrecognized intent action: " + action);
    114         }
    115         return START_NOT_STICKY;
    116     }
    117 
    118     private void handleCellBroadcastIntent(Intent intent) {
    119         Bundle extras = intent.getExtras();
    120         if (extras == null) {
    121             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
    122             return;
    123         }
    124 
    125         SmsCbMessage message = (SmsCbMessage) extras.get("message");
    126 
    127         if (message == null) {
    128             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
    129             return;
    130         }
    131 
    132         final CellBroadcastMessage cbm = new CellBroadcastMessage(message);
    133         if (!isMessageEnabledByUser(cbm)) {
    134             Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() +
    135                     " by user preference");
    136             return;
    137         }
    138 
    139         // Set.add() returns false if message ID has already been added
    140         MessageIdAndScope messageIdAndScope = new MessageIdAndScope(message.getSerialNumber(),
    141                 message.getLocation());
    142         if (!sCmasIdList.add(messageIdAndScope)) {
    143             Log.d(TAG, "ignoring duplicate alert with " + messageIdAndScope);
    144             return;
    145         }
    146 
    147         final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
    148         alertIntent.setClass(this, CellBroadcastAlertService.class);
    149         alertIntent.putExtra("message", cbm);
    150 
    151         // write to database on a background thread
    152         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
    153                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
    154                     @Override
    155                     public boolean execute(CellBroadcastContentProvider provider) {
    156                         if (provider.insertNewBroadcast(cbm)) {
    157                             // new message, show the alert or notification on UI thread
    158                             startService(alertIntent);
    159                             return true;
    160                         } else {
    161                             return false;
    162                         }
    163                     }
    164                 });
    165     }
    166 
    167     private void showNewAlert(Intent intent) {
    168         Bundle extras = intent.getExtras();
    169         if (extras == null) {
    170             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
    171             return;
    172         }
    173 
    174         CellBroadcastMessage cbm = (CellBroadcastMessage) extras.get("message");
    175 
    176         if (cbm == null) {
    177             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
    178             return;
    179         }
    180 
    181         if (cbm.isEmergencyAlertMessage() || CellBroadcastConfigService
    182                 .isOperatorDefinedEmergencyId(cbm.getServiceCategory())) {
    183             // start alert sound / vibration / TTS and display full-screen alert
    184             openEmergencyAlertNotification(cbm);
    185         } else {
    186             // add notification to the bar
    187             addToNotificationBar(cbm);
    188         }
    189     }
    190 
    191     /**
    192      * Filter out broadcasts on the test channels that the user has not enabled,
    193      * and types of notifications that the user is not interested in receiving.
    194      * This allows us to enable an entire range of message identifiers in the
    195      * radio and not have to explicitly disable the message identifiers for
    196      * test broadcasts. In the unlikely event that the default shared preference
    197      * values were not initialized in CellBroadcastReceiverApp, the second parameter
    198      * to the getBoolean() calls match the default values in res/xml/preferences.xml.
    199      *
    200      * @param message the message to check
    201      * @return true if the user has enabled this message type; false otherwise
    202      */
    203     private boolean isMessageEnabledByUser(CellBroadcastMessage message) {
    204         if (message.isEtwsTestMessage()) {
    205             return PreferenceManager.getDefaultSharedPreferences(this)
    206                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false);
    207         }
    208 
    209         if (message.isCmasMessage()) {
    210             switch (message.getCmasMessageClass()) {
    211                 case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
    212                     return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    213                             CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
    214 
    215                 case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
    216                     return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    217                             CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
    218 
    219                 case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
    220                     return PreferenceManager.getDefaultSharedPreferences(this)
    221                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
    222 
    223                 case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
    224                 case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
    225                     return PreferenceManager.getDefaultSharedPreferences(this)
    226                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, false);
    227 
    228                 default:
    229                     return true;    // presidential-level CMAS alerts are always enabled
    230             }
    231         }
    232 
    233         return true;    // other broadcast messages are always enabled
    234     }
    235 
    236     private void acquireTimedWakelock(int timeout) {
    237         if (mWakeLock == null) {
    238             PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
    239             // Note: acquiring a PARTIAL_WAKE_LOCK and setting window flag FLAG_TURN_SCREEN_ON in
    240             // CellBroadcastAlertFullScreen is not sufficient to turn on the screen by itself.
    241             // Use SCREEN_BRIGHT_WAKE_LOCK here as a workaround to ensure the screen turns on.
    242             mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK
    243                     | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG);
    244         }
    245         mWakeLock.acquire(timeout);
    246     }
    247 
    248     /**
    249      * Display a full-screen alert message for emergency alerts.
    250      * @param message the alert to display
    251      */
    252     private void openEmergencyAlertNotification(CellBroadcastMessage message) {
    253         // Acquire a CPU wake lock until the alert dialog and audio start playing.
    254         acquireTimedWakelock(WAKE_LOCK_TIMEOUT);
    255 
    256         // Close dialogs and window shade
    257         Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
    258         sendBroadcast(closeDialogs);
    259 
    260         // start audio/vibration/speech service for emergency alerts
    261         Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
    262         audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
    263         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    264         String duration = prefs.getString(CellBroadcastSettings.KEY_ALERT_SOUND_DURATION,
    265                 CellBroadcastSettings.ALERT_SOUND_DEFAULT_DURATION);
    266         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION_EXTRA,
    267                 Integer.parseInt(duration));
    268 
    269         int channelTitleId = CellBroadcastResources.getDialogTitleResource(message);
    270         CharSequence channelName = getText(channelTitleId);
    271         String messageBody = message.getMessageBody();
    272 
    273         if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) {
    274             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
    275 
    276             String language = message.getLanguageCode();
    277             if (message.isEtwsMessage() && !"ja".equals(language)) {
    278                 Log.w(TAG, "bad language code for ETWS - using Japanese TTS");
    279                 language = "ja";
    280             } else if (message.isCmasMessage() && !"en".equals(language)) {
    281                 Log.w(TAG, "bad language code for CMAS - using English TTS");
    282                 language = "en";
    283             }
    284             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE,
    285                     language);
    286         }
    287         startService(audioIntent);
    288 
    289         // Use lower 32 bits of emergency alert delivery time for notification ID
    290         int notificationId = (int) message.getDeliveryTime();
    291 
    292         // Decide which activity to start based on the state of the keyguard.
    293         Class c = CellBroadcastAlertDialog.class;
    294         KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
    295         if (km.inKeyguardRestrictedInputMode()) {
    296             // Use the full screen activity for security.
    297             c = CellBroadcastAlertFullScreen.class;
    298         }
    299 
    300         Intent notify = createDisplayMessageIntent(this, c, message, notificationId);
    301         PendingIntent pi = PendingIntent.getActivity(this, notificationId, notify, 0);
    302 
    303         Notification.Builder builder = new Notification.Builder(this)
    304                 .setSmallIcon(R.drawable.ic_notify_alert)
    305                 .setTicker(getText(CellBroadcastResources.getDialogTitleResource(message)))
    306                 .setWhen(System.currentTimeMillis())
    307                 .setContentIntent(pi)
    308                 .setFullScreenIntent(pi, true)
    309                 .setContentTitle(channelName)
    310                 .setContentText(messageBody)
    311                 .setDefaults(Notification.DEFAULT_LIGHTS);
    312 
    313         NotificationManager notificationManager =
    314             (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
    315 
    316         notificationManager.notify(notificationId, builder.getNotification());
    317     }
    318 
    319     /**
    320      * Add the new alert to the notification bar (non-emergency alerts), or launch a
    321      * high-priority immediate intent for emergency alerts.
    322      * @param message the alert to display
    323      */
    324     private void addToNotificationBar(CellBroadcastMessage message) {
    325         int channelTitleId = CellBroadcastResources.getDialogTitleResource(message);
    326         CharSequence channelName = getText(channelTitleId);
    327         String messageBody = message.getMessageBody();
    328 
    329         // Use the same ID to create a single notification for multiple non-emergency alerts.
    330         int notificationId = NOTIFICATION_ID;
    331 
    332         PendingIntent pi = PendingIntent.getActivity(this, 0, createDisplayMessageIntent(
    333                 this, CellBroadcastListActivity.class, message, notificationId), 0);
    334 
    335         // use default sound/vibration/lights for non-emergency broadcasts
    336         Notification.Builder builder = new Notification.Builder(this)
    337                 .setSmallIcon(R.drawable.ic_notify_alert)
    338                 .setTicker(channelName)
    339                 .setWhen(System.currentTimeMillis())
    340                 .setContentIntent(pi)
    341                 .setDefaults(Notification.DEFAULT_ALL);
    342 
    343         builder.setDefaults(Notification.DEFAULT_ALL);
    344 
    345         // increment unread alert count (decremented when user dismisses alert dialog)
    346         int unreadCount = CellBroadcastReceiverApp.incrementUnreadAlertCount();
    347         if (unreadCount > 1) {
    348             // use generic count of unread broadcasts if more than one unread
    349             builder.setContentTitle(getString(R.string.notification_multiple_title));
    350             builder.setContentText(getString(R.string.notification_multiple, unreadCount));
    351         } else {
    352             builder.setContentTitle(channelName).setContentText(messageBody);
    353         }
    354 
    355         Log.i(TAG, "addToNotificationBar notificationId: " + notificationId);
    356 
    357         NotificationManager notificationManager =
    358             (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
    359 
    360         notificationManager.notify(notificationId, builder.getNotification());
    361     }
    362 
    363     static Intent createDisplayMessageIntent(Context context, Class intentClass,
    364             CellBroadcastMessage message, int notificationId) {
    365         // Trigger the list activity to fire up a dialog that shows the received messages
    366         Intent intent = new Intent(context, intentClass);
    367         intent.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, message);
    368         intent.putExtra(SMS_CB_NOTIFICATION_ID_EXTRA, notificationId);
    369         intent.putExtra(NEW_ALERT_EXTRA, true);
    370 
    371         // This line is needed to make this intent compare differently than the other intents
    372         // created here for other messages. Without this line, the PendingIntent always gets the
    373         // intent of a previous message and notification.
    374         intent.setType(Integer.toString(notificationId));
    375 
    376         return intent;
    377     }
    378 
    379     @Override
    380     public IBinder onBind(Intent intent) {
    381         return null;    // clients can't bind to this service
    382     }
    383 }
    384