• 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.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 
MessageServiceCategoryAndScope(int serviceCategory, int serialNumber, SmsCbLocation location)68         MessageServiceCategoryAndScope(int serviceCategory, int serialNumber,
69                 SmsCbLocation location) {
70             mServiceCategory = serviceCategory;
71             mSerialNumber = serialNumber;
72             mLocation = location;
73         }
74 
75         @Override
hashCode()76         public int hashCode() {
77             return mLocation.hashCode() + 5 * mServiceCategory + 7 * mSerialNumber;
78         }
79 
80         @Override
equals(Object o)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
toString()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
onStartCommand(Intent intent, int flags, int startId)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 
handleCellBroadcastIntent(Intent intent)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 
showNewAlert(Intent intent)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      */
isMessageEnabledByUser(CellBroadcastMessage message)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      */
openEmergencyAlertNotification(CellBroadcastMessage message)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      */
addToNotificationBar(CellBroadcastMessage message)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 
createDisplayMessageIntent(Context context, Class intentClass, ArrayList<CellBroadcastMessage> messageList)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
onBind(Intent intent)418     public IBinder onBind(Intent intent) {
419         return null;    // clients can't bind to this service
420     }
421 }
422