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