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