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