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