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.app.ActivityManagerNative;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.SharedPreferences;
     28 import android.os.Bundle;
     29 import android.os.IBinder;
     30 import android.os.RemoteException;
     31 import android.os.UserHandle;
     32 import android.preference.PreferenceManager;
     33 import android.provider.Telephony;
     34 import android.telephony.CellBroadcastMessage;
     35 import android.telephony.SmsCbCmasInfo;
     36 import android.telephony.SmsCbLocation;
     37 import android.telephony.SmsCbMessage;
     38 import android.util.Log;
     39 
     40 import java.util.ArrayList;
     41 import java.util.HashSet;
     42 
     43 /**
     44  * This service manages the display and animation of broadcast messages.
     45  * Emergency messages display with a flashing animated exclamation mark icon,
     46  * and an alert tone is played when the alert is first shown to the user
     47  * (but not when the user views a previously received broadcast).
     48  */
     49 public class CellBroadcastAlertService extends Service {
     50     private static final String TAG = "CellBroadcastAlertService";
     51 
     52     /** Intent action to display alert dialog/notification, after verifying the alert is new. */
     53     static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
     54 
     55     /** Use the same notification ID for non-emergency alerts. */
     56     static final int NOTIFICATION_ID = 1;
     57 
     58     /** Sticky broadcast for latest area info broadcast received. */
     59     static final String CB_AREA_INFO_RECEIVED_ACTION =
     60             "android.cellbroadcastreceiver.CB_AREA_INFO_RECEIVED";
     61 
     62     /** Container for message ID and geographical scope, for duplicate message detection. */
     63     private static final class MessageServiceCategoryAndScope {
     64         private final int mServiceCategory;
     65         private final int mSerialNumber;
     66         private final SmsCbLocation mLocation;
     67 
     68         MessageServiceCategoryAndScope(int serviceCategory, int serialNumber,
     69                 SmsCbLocation location) {
     70             mServiceCategory = serviceCategory;
     71             mSerialNumber = serialNumber;
     72             mLocation = location;
     73         }
     74 
     75         @Override
     76         public int hashCode() {
     77             return mLocation.hashCode() + 5 * mServiceCategory + 7 * mSerialNumber;
     78         }
     79 
     80         @Override
     81         public boolean equals(Object o) {
     82             if (o == this) {
     83                 return true;
     84             }
     85             if (o instanceof MessageServiceCategoryAndScope) {
     86                 MessageServiceCategoryAndScope other = (MessageServiceCategoryAndScope) o;
     87                 return (mServiceCategory == other.mServiceCategory &&
     88                         mSerialNumber == other.mSerialNumber &&
     89                         mLocation.equals(other.mLocation));
     90             }
     91             return false;
     92         }
     93 
     94         @Override
     95         public String toString() {
     96             return "{mServiceCategory: " + mServiceCategory + " serial number: " + mSerialNumber +
     97                     " location: " + mLocation.toString() + '}';
     98         }
     99     }
    100 
    101     /** Cache of received message IDs, for duplicate message detection. */
    102     private static final HashSet<MessageServiceCategoryAndScope> sCmasIdSet =
    103             new HashSet<MessageServiceCategoryAndScope>(8);
    104 
    105     /** Maximum number of message IDs to save before removing the oldest message ID. */
    106     private static final int MAX_MESSAGE_ID_SIZE = 65535;
    107 
    108     /** List of message IDs received, for removing oldest ID when max message IDs are received. */
    109     private static final ArrayList<MessageServiceCategoryAndScope> sCmasIdList =
    110             new ArrayList<MessageServiceCategoryAndScope>(8);
    111 
    112     /** Index of message ID to replace with new message ID when max message IDs are received. */
    113     private static int sCmasIdListIndex = 0;
    114 
    115     @Override
    116     public int onStartCommand(Intent intent, int flags, int startId) {
    117         String action = intent.getAction();
    118         if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) ||
    119                 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
    120             handleCellBroadcastIntent(intent);
    121         } else if (SHOW_NEW_ALERT_ACTION.equals(action)) {
    122             try {
    123                 if (UserHandle.myUserId() ==
    124                         ActivityManagerNative.getDefault().getCurrentUser().id) {
    125                     showNewAlert(intent);
    126                 } else {
    127                     Log.d(TAG,"Not active user, ignore the alert display");
    128                 }
    129             } catch (RemoteException e) {
    130                 e.printStackTrace();
    131             }
    132         } else {
    133             Log.e(TAG, "Unrecognized intent action: " + action);
    134         }
    135         return START_NOT_STICKY;
    136     }
    137 
    138     private void handleCellBroadcastIntent(Intent intent) {
    139         Bundle extras = intent.getExtras();
    140         if (extras == null) {
    141             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
    142             return;
    143         }
    144 
    145         SmsCbMessage message = (SmsCbMessage) extras.get("message");
    146 
    147         if (message == null) {
    148             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
    149             return;
    150         }
    151 
    152         final CellBroadcastMessage cbm = new CellBroadcastMessage(message);
    153         if (!isMessageEnabledByUser(cbm)) {
    154             Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() +
    155                     " by user preference");
    156             return;
    157         }
    158 
    159         // Check for duplicate message IDs according to CMAS carrier requirements. Message IDs
    160         // are stored in volatile memory. If the maximum of 65535 messages is reached, the
    161         // message ID of the oldest message is deleted from the list.
    162         MessageServiceCategoryAndScope newCmasId = new MessageServiceCategoryAndScope(
    163                 message.getServiceCategory(), message.getSerialNumber(), message.getLocation());
    164 
    165         // Add the new message ID to the list. It's okay if this is a duplicate message ID,
    166         // because the list is only used for removing old message IDs from the hash set.
    167         if (sCmasIdList.size() < MAX_MESSAGE_ID_SIZE) {
    168             sCmasIdList.add(newCmasId);
    169         } else {
    170             // Get oldest message ID from the list and replace with the new message ID.
    171             MessageServiceCategoryAndScope oldestCmasId = sCmasIdList.get(sCmasIdListIndex);
    172             sCmasIdList.set(sCmasIdListIndex, newCmasId);
    173             Log.d(TAG, "message ID limit reached, removing oldest message ID " + oldestCmasId);
    174             // Remove oldest message ID from the set.
    175             sCmasIdSet.remove(oldestCmasId);
    176             if (++sCmasIdListIndex >= MAX_MESSAGE_ID_SIZE) {
    177                 sCmasIdListIndex = 0;
    178             }
    179         }
    180         // Set.add() returns false if message ID has already been added
    181         if (!sCmasIdSet.add(newCmasId)) {
    182             Log.d(TAG, "ignoring duplicate alert with " + newCmasId);
    183             return;
    184         }
    185 
    186         final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
    187         alertIntent.setClass(this, CellBroadcastAlertService.class);
    188         alertIntent.putExtra("message", cbm);
    189 
    190         // write to database on a background thread
    191         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
    192                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
    193                     @Override
    194                     public boolean execute(CellBroadcastContentProvider provider) {
    195                         if (provider.insertNewBroadcast(cbm)) {
    196                             // new message, show the alert or notification on UI thread
    197                             startService(alertIntent);
    198                             return true;
    199                         } else {
    200                             return false;
    201                         }
    202                     }
    203                 });
    204     }
    205 
    206     private void showNewAlert(Intent intent) {
    207         Bundle extras = intent.getExtras();
    208         if (extras == null) {
    209             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
    210             return;
    211         }
    212 
    213         CellBroadcastMessage cbm = (CellBroadcastMessage) extras.get("message");
    214 
    215         if (cbm == null) {
    216             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
    217             return;
    218         }
    219 
    220         if (CellBroadcastConfigService.isEmergencyAlertMessage(cbm)) {
    221             // start alert sound / vibration / TTS and display full-screen alert
    222             openEmergencyAlertNotification(cbm);
    223         } else {
    224             // add notification to the bar
    225             addToNotificationBar(cbm);
    226         }
    227     }
    228 
    229     /**
    230      * Filter out broadcasts on the test channels that the user has not enabled,
    231      * and types of notifications that the user is not interested in receiving.
    232      * This allows us to enable an entire range of message identifiers in the
    233      * radio and not have to explicitly disable the message identifiers for
    234      * test broadcasts. In the unlikely event that the default shared preference
    235      * values were not initialized in CellBroadcastReceiverApp, the second parameter
    236      * to the getBoolean() calls match the default values in res/xml/preferences.xml.
    237      *
    238      * @param message the message to check
    239      * @return true if the user has enabled this message type; false otherwise
    240      */
    241     private boolean isMessageEnabledByUser(CellBroadcastMessage message) {
    242         if (message.isEtwsTestMessage()) {
    243             return PreferenceManager.getDefaultSharedPreferences(this)
    244                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false);
    245         }
    246 
    247         if (message.isCmasMessage()) {
    248             switch (message.getCmasMessageClass()) {
    249                 case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
    250                     return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    251                             CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
    252 
    253                 case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
    254                     return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
    255                             CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
    256 
    257                 case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
    258                     return PreferenceManager.getDefaultSharedPreferences(this)
    259                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
    260 
    261                 case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
    262                 case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
    263                 case SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE:
    264                     return PreferenceManager.getDefaultSharedPreferences(this)
    265                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, false);
    266 
    267                 default:
    268                     return true;    // presidential-level CMAS alerts are always enabled
    269             }
    270         }
    271 
    272         if (message.getServiceCategory() == 50) {
    273             // save latest area info broadcast for Settings display and send as broadcast
    274             CellBroadcastReceiverApp.setLatestAreaInfo(message);
    275             Intent intent = new Intent(CB_AREA_INFO_RECEIVED_ACTION);
    276             intent.putExtra("message", message);
    277             sendBroadcastAsUser(intent, UserHandle.ALL,
    278                     android.Manifest.permission.READ_PHONE_STATE);
    279             return false;   // area info broadcasts are displayed in Settings status screen
    280         }
    281 
    282         return true;    // other broadcast messages are always enabled
    283     }
    284 
    285     /**
    286      * Display a full-screen alert message for emergency alerts.
    287      * @param message the alert to display
    288      */
    289     private void openEmergencyAlertNotification(CellBroadcastMessage message) {
    290         // Acquire a CPU wake lock until the alert dialog and audio start playing.
    291         CellBroadcastAlertWakeLock.acquireScreenCpuWakeLock(this);
    292 
    293         // Close dialogs and window shade
    294         Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
    295         sendBroadcast(closeDialogs);
    296 
    297         // start audio/vibration/speech service for emergency alerts
    298         Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
    299         audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
    300         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    301 
    302         int duration;   // alert audio duration in ms
    303         if (message.isCmasMessage()) {
    304             // CMAS requirement: duration of the audio attention signal is 10.5 seconds.
    305             duration = 10500;
    306         } else {
    307             duration = Integer.parseInt(prefs.getString(
    308                     CellBroadcastSettings.KEY_ALERT_SOUND_DURATION,
    309                     CellBroadcastSettings.ALERT_SOUND_DEFAULT_DURATION)) * 1000;
    310         }
    311         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION_EXTRA, duration);
    312 
    313         if (message.isEtwsMessage()) {
    314             // For ETWS, always vibrate, even in silent mode.
    315             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA, true);
    316             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_ETWS_VIBRATE_EXTRA, true);
    317         } else {
    318             // For other alerts, vibration can be disabled in app settings.
    319             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA,
    320                     prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true));
    321         }
    322 
    323         String messageBody = message.getMessageBody();
    324 
    325         if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) {
    326             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
    327 
    328             String language = message.getLanguageCode();
    329             if (message.isEtwsMessage() && !"ja".equals(language)) {
    330                 Log.w(TAG, "bad language code for ETWS - using Japanese TTS");
    331                 language = "ja";
    332             } else if (message.isCmasMessage() && !"en".equals(language)) {
    333                 Log.w(TAG, "bad language code for CMAS - using English TTS");
    334                 language = "en";
    335             }
    336             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE,
    337                     language);
    338         }
    339         startService(audioIntent);
    340 
    341         // Decide which activity to start based on the state of the keyguard.
    342         Class c = CellBroadcastAlertDialog.class;
    343         KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
    344         if (km.inKeyguardRestrictedInputMode()) {
    345             // Use the full screen activity for security.
    346             c = CellBroadcastAlertFullScreen.class;
    347         }
    348 
    349         ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1);
    350         messageList.add(message);
    351 
    352         Intent alertDialogIntent = createDisplayMessageIntent(this, c, messageList);
    353         alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    354         startActivity(alertDialogIntent);
    355     }
    356 
    357     /**
    358      * Add the new alert to the notification bar (non-emergency alerts), or launch a
    359      * high-priority immediate intent for emergency alerts.
    360      * @param message the alert to display
    361      */
    362     private void addToNotificationBar(CellBroadcastMessage message) {
    363         int channelTitleId = CellBroadcastResources.getDialogTitleResource(message);
    364         CharSequence channelName = getText(channelTitleId);
    365         String messageBody = message.getMessageBody();
    366 
    367         // Pass the list of unread non-emergency CellBroadcastMessages
    368         ArrayList<CellBroadcastMessage> messageList = CellBroadcastReceiverApp
    369                 .addNewMessageToList(message);
    370 
    371         // Create intent to show the new messages when user selects the notification.
    372         Intent intent = createDisplayMessageIntent(this, CellBroadcastAlertDialog.class,
    373                 messageList);
    374         intent.putExtra(CellBroadcastAlertFullScreen.FROM_NOTIFICATION_EXTRA, true);
    375 
    376         PendingIntent pi = PendingIntent.getActivity(this, NOTIFICATION_ID, intent,
    377                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    378 
    379         // use default sound/vibration/lights for non-emergency broadcasts
    380         Notification.Builder builder = new Notification.Builder(this)
    381                 .setSmallIcon(R.drawable.ic_notify_alert)
    382                 .setTicker(channelName)
    383                 .setWhen(System.currentTimeMillis())
    384                 .setContentIntent(pi)
    385                 .setCategory(Notification.CATEGORY_SYSTEM)
    386                 .setPriority(Notification.PRIORITY_HIGH)
    387                 .setColor(getResources().getColor(R.color.notification_color))
    388                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    389                 .setDefaults(Notification.DEFAULT_ALL);
    390 
    391         builder.setDefaults(Notification.DEFAULT_ALL);
    392 
    393         // increment unread alert count (decremented when user dismisses alert dialog)
    394         int unreadCount = messageList.size();
    395         if (unreadCount > 1) {
    396             // use generic count of unread broadcasts if more than one unread
    397             builder.setContentTitle(getString(R.string.notification_multiple_title));
    398             builder.setContentText(getString(R.string.notification_multiple, unreadCount));
    399         } else {
    400             builder.setContentTitle(channelName).setContentText(messageBody);
    401         }
    402 
    403         NotificationManager notificationManager =
    404             (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
    405 
    406         notificationManager.notify(NOTIFICATION_ID, builder.build());
    407     }
    408 
    409     static Intent createDisplayMessageIntent(Context context, Class intentClass,
    410             ArrayList<CellBroadcastMessage> messageList) {
    411         // Trigger the list activity to fire up a dialog that shows the received messages
    412         Intent intent = new Intent(context, intentClass);
    413         intent.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, messageList);
    414         return intent;
    415     }
    416 
    417     @Override
    418     public IBinder onBind(Intent intent) {
    419         return null;    // clients can't bind to this service
    420     }
    421 }
    422