• 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.ActivityManagerNative;
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.Binder;
28 import android.os.Bundle;
29 import android.os.IBinder;
30 import android.os.PersistableBundle;
31 import android.os.RemoteException;
32 import android.os.SystemClock;
33 import android.os.UserHandle;
34 import android.preference.PreferenceManager;
35 import android.provider.Telephony;
36 import android.telephony.CarrierConfigManager;
37 import android.telephony.CellBroadcastMessage;
38 import android.telephony.SmsCbCmasInfo;
39 import android.telephony.SmsCbEtwsInfo;
40 import android.telephony.SmsCbLocation;
41 import android.telephony.SmsCbMessage;
42 import android.telephony.SubscriptionManager;
43 import android.util.Log;
44 
45 import com.android.cellbroadcastreceiver.CellBroadcastAlertAudio.ToneType;
46 import com.android.cellbroadcastreceiver.CellBroadcastOtherChannelsManager.CellBroadcastChannelRange;
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.telephony.PhoneConstants;
49 
50 import java.util.ArrayList;
51 import java.util.LinkedHashMap;
52 import java.util.Locale;
53 
54 import static android.text.format.DateUtils.DAY_IN_MILLIS;
55 
56 /**
57  * This service manages the display and animation of broadcast messages.
58  * Emergency messages display with a flashing animated exclamation mark icon,
59  * and an alert tone is played when the alert is first shown to the user
60  * (but not when the user views a previously received broadcast).
61  */
62 public class CellBroadcastAlertService extends Service {
63     private static final String TAG = "CBAlertService";
64 
65     /** Intent action to display alert dialog/notification, after verifying the alert is new. */
66     static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
67 
68     /** Use the same notification ID for non-emergency alerts. */
69     static final int NOTIFICATION_ID = 1;
70 
71     /** Sticky broadcast for latest area info broadcast received. */
72     static final String CB_AREA_INFO_RECEIVED_ACTION =
73             "android.cellbroadcastreceiver.CB_AREA_INFO_RECEIVED";
74 
75     /**
76      * Default message expiration time is 24 hours. Same message arrives within 24 hours will be
77      * treated as a duplicate.
78      */
79     private static final long DEFAULT_EXPIRATION_TIME = DAY_IN_MILLIS;
80 
81     /**
82      *  Container for service category, serial number, location, body hash code, and ETWS primary/
83      *  secondary information for duplication detection.
84      */
85     private static final class MessageServiceCategoryAndScope {
86         private final int mServiceCategory;
87         private final int mSerialNumber;
88         private final SmsCbLocation mLocation;
89         private final int mBodyHash;
90         private final boolean mIsEtwsPrimary;
91 
MessageServiceCategoryAndScope(int serviceCategory, int serialNumber, SmsCbLocation location, int bodyHash, boolean isEtwsPrimary)92         MessageServiceCategoryAndScope(int serviceCategory, int serialNumber,
93                 SmsCbLocation location, int bodyHash, boolean isEtwsPrimary) {
94             mServiceCategory = serviceCategory;
95             mSerialNumber = serialNumber;
96             mLocation = location;
97             mBodyHash = bodyHash;
98             mIsEtwsPrimary = isEtwsPrimary;
99         }
100 
101         @Override
hashCode()102         public int hashCode() {
103             return mLocation.hashCode() + 5 * mServiceCategory + 7 * mSerialNumber + 13 * mBodyHash
104                     + 17 * Boolean.hashCode(mIsEtwsPrimary);
105         }
106 
107         @Override
equals(Object o)108         public boolean equals(Object o) {
109             if (o == this) {
110                 return true;
111             }
112             if (o instanceof MessageServiceCategoryAndScope) {
113                 MessageServiceCategoryAndScope other = (MessageServiceCategoryAndScope) o;
114                 return (mServiceCategory == other.mServiceCategory &&
115                         mSerialNumber == other.mSerialNumber &&
116                         mLocation.equals(other.mLocation) &&
117                         mBodyHash == other.mBodyHash &&
118                         mIsEtwsPrimary == other.mIsEtwsPrimary);
119             }
120             return false;
121         }
122 
123         @Override
toString()124         public String toString() {
125             return "{mServiceCategory: " + mServiceCategory + " serial number: " + mSerialNumber +
126                     " location: " + mLocation.toString() + " body hash: " + mBodyHash +
127                     " mIsEtwsPrimary: " + mIsEtwsPrimary + "}";
128         }
129     }
130 
131     /** Maximum number of message IDs to save before removing the oldest message ID. */
132     private static final int MAX_MESSAGE_ID_SIZE = 1024;
133 
134     /** Linked hash map of the message identities for duplication detection purposes. The key is the
135      * the collection of different message keys used for duplication detection, and the value
136      * is the timestamp of message arriving time. Some carriers may require shorter expiration time.
137      */
138     private static final LinkedHashMap<MessageServiceCategoryAndScope, Long> sMessagesMap =
139             new LinkedHashMap<>();
140 
141     @Override
onStartCommand(Intent intent, int flags, int startId)142     public int onStartCommand(Intent intent, int flags, int startId) {
143         String action = intent.getAction();
144         Log.d(TAG, "onStartCommand: " + action);
145         if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) ||
146                 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
147             handleCellBroadcastIntent(intent);
148         } else if (SHOW_NEW_ALERT_ACTION.equals(action)) {
149             try {
150                 if (UserHandle.myUserId() ==
151                         ActivityManagerNative.getDefault().getCurrentUser().id) {
152                     showNewAlert(intent);
153                 } else {
154                     Log.d(TAG,"Not active user, ignore the alert display");
155                 }
156             } catch (RemoteException e) {
157                 e.printStackTrace();
158             }
159         } else {
160             Log.e(TAG, "Unrecognized intent action: " + action);
161         }
162         return START_NOT_STICKY;
163     }
164 
165     /**
166      * Get the carrier specific message duplicate expiration time.
167      *
168      * @param subId Subscription index
169      * @return The expiration time in milliseconds. Small values like 0 (or negative values)
170      * indicate expiration immediately (meaning the duplicate will always be displayed), while large
171      * values indicate the duplicate will always be ignored. The default value would be 24 hours.
172      */
getDuplicateExpirationTime(int subId)173     private long getDuplicateExpirationTime(int subId) {
174         CarrierConfigManager configManager = (CarrierConfigManager)
175                 getApplicationContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
176         Log.d(TAG, "manager = " + configManager);
177         if (configManager == null) {
178             Log.e(TAG, "carrier config is not available.");
179             return DEFAULT_EXPIRATION_TIME;
180         }
181 
182         PersistableBundle b = configManager.getConfigForSubId(subId);
183         if (b == null) {
184             Log.e(TAG, "expiration key does not exist.");
185             return DEFAULT_EXPIRATION_TIME;
186         }
187 
188         long time = b.getLong(CarrierConfigManager.KEY_MESSAGE_EXPIRATION_TIME_LONG,
189                 DEFAULT_EXPIRATION_TIME);
190         return time;
191     }
192 
handleCellBroadcastIntent(Intent intent)193     private void handleCellBroadcastIntent(Intent intent) {
194         Bundle extras = intent.getExtras();
195         if (extras == null) {
196             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
197             return;
198         }
199 
200         SmsCbMessage message = (SmsCbMessage) extras.get("message");
201 
202         if (message == null) {
203             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
204             return;
205         }
206 
207         final CellBroadcastMessage cbm = new CellBroadcastMessage(message);
208         int subId = intent.getExtras().getInt(PhoneConstants.SUBSCRIPTION_KEY);
209         if (SubscriptionManager.isValidSubscriptionId(subId)) {
210             cbm.setSubId(subId);
211         } else {
212             Log.e(TAG, "Invalid subscription id");
213         }
214 
215         if (!isMessageEnabledByUser(cbm)) {
216             Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() +
217                     " by user preference");
218             return;
219         }
220 
221         // If this is an ETWS message, then we want to include the body message to be a factor for
222         // duplication detection. We found that some Japanese carriers send ETWS messages
223         // with the same serial number, therefore the subsequent messages were all ignored.
224         // In the other hand, US carriers have the requirement that only serial number, location,
225         // and category should be used for duplicate detection.
226         int hashCode = message.isEtwsMessage() ? message.getMessageBody().hashCode() : 0;
227 
228         // If this is an ETWS message, we need to include primary/secondary message information to
229         // be a factor for duplication detection as well. Per 3GPP TS 23.041 section 8.2,
230         // duplicate message detection shall be performed independently for primary and secondary
231         // notifications.
232         boolean isEtwsPrimary = false;
233         if (message.isEtwsMessage()) {
234             SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo();
235             if (etwsInfo != null) {
236                 isEtwsPrimary = etwsInfo.isPrimary();
237             } else {
238                 Log.w(TAG, "ETWS info is not available.");
239             }
240         }
241 
242         // Check for duplicate message IDs according to CMAS carrier requirements. Message IDs
243         // are stored in volatile memory. If the maximum of 1024 messages is reached, the
244         // message ID of the oldest message is deleted from the list.
245         MessageServiceCategoryAndScope newCmasId = new MessageServiceCategoryAndScope(
246                 message.getServiceCategory(), message.getSerialNumber(), message.getLocation(),
247                 hashCode, isEtwsPrimary);
248 
249         Log.d(TAG, "message ID = " + newCmasId);
250 
251         long nowTime = SystemClock.elapsedRealtime();
252         // Check if the identical message arrives again
253         if (sMessagesMap.get(newCmasId) != null) {
254             // And if the previous one has not expired yet, treat it as a duplicate message.
255             long previousTime = sMessagesMap.get(newCmasId);
256             long expirationTime = getDuplicateExpirationTime(subId);
257             if (nowTime - previousTime < expirationTime) {
258                 Log.d(TAG, "ignoring the duplicate alert " + newCmasId + ", nowTime=" + nowTime
259                         + ", previous=" + previousTime + ", expiration=" + expirationTime);
260                 return;
261             }
262             // otherwise, we don't treat it as a duplicate and will show the same message again.
263             Log.d(TAG, "The same message shown up " + (nowTime - previousTime)
264                     + " milliseconds ago. Not a duplicate.");
265         } else if (sMessagesMap.size() >= MAX_MESSAGE_ID_SIZE){
266             // If we reach the maximum, remove the first inserted message key.
267             MessageServiceCategoryAndScope oldestCmasId = sMessagesMap.keySet().iterator().next();
268             Log.d(TAG, "message ID limit reached, removing oldest message ID " + oldestCmasId);
269             sMessagesMap.remove(oldestCmasId);
270         } else {
271             Log.d(TAG, "New message. Not a duplicate. Map size = " + sMessagesMap.size());
272         }
273 
274         sMessagesMap.put(newCmasId, nowTime);
275 
276         final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
277         alertIntent.setClass(this, CellBroadcastAlertService.class);
278         alertIntent.putExtra("message", cbm);
279 
280         // write to database on a background thread
281         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
282                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
283                     @Override
284                     public boolean execute(CellBroadcastContentProvider provider) {
285                         if (provider.insertNewBroadcast(cbm)) {
286                             // new message, show the alert or notification on UI thread
287                             startService(alertIntent);
288                             return true;
289                         } else {
290                             return false;
291                         }
292                     }
293                 });
294     }
295 
showNewAlert(Intent intent)296     private void showNewAlert(Intent intent) {
297         Bundle extras = intent.getExtras();
298         if (extras == null) {
299             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
300             return;
301         }
302 
303         CellBroadcastMessage cbm = (CellBroadcastMessage) intent.getParcelableExtra("message");
304 
305         if (cbm == null) {
306             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
307             return;
308         }
309 
310         if (isEmergencyMessage(this, cbm)) {
311             // start alert sound / vibration / TTS and display full-screen alert
312             openEmergencyAlertNotification(cbm);
313         } else {
314             // add notification to the bar by passing the list of unread non-emergency
315             // CellBroadcastMessages
316             ArrayList<CellBroadcastMessage> messageList = CellBroadcastReceiverApp
317                     .addNewMessageToList(cbm);
318             addToNotificationBar(cbm, messageList, this, false);
319         }
320     }
321 
322     /**
323      * Filter out broadcasts on the test channels that the user has not enabled,
324      * and types of notifications that the user is not interested in receiving.
325      * This allows us to enable an entire range of message identifiers in the
326      * radio and not have to explicitly disable the message identifiers for
327      * test broadcasts. In the unlikely event that the default shared preference
328      * values were not initialized in CellBroadcastReceiverApp, the second parameter
329      * to the getBoolean() calls match the default values in res/xml/preferences.xml.
330      *
331      * @param message the message to check
332      * @return true if the user has enabled this message type; false otherwise
333      */
isMessageEnabledByUser(CellBroadcastMessage message)334     private boolean isMessageEnabledByUser(CellBroadcastMessage message) {
335 
336         // Check if all emergency alerts are disabled.
337         boolean emergencyAlertEnabled = PreferenceManager.getDefaultSharedPreferences(this).
338                 getBoolean(CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS, true);
339 
340         // Check if ETWS/CMAS test message is forced to disabled on the device.
341         boolean forceDisableEtwsCmasTest =
342                 CellBroadcastSettings.isFeatureEnabled(this,
343                         CarrierConfigManager.KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false);
344 
345         if (message.isEtwsTestMessage()) {
346             return emergencyAlertEnabled &&
347                     !forceDisableEtwsCmasTest &&
348                     PreferenceManager.getDefaultSharedPreferences(this)
349                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false);
350         }
351 
352         if (message.isEtwsMessage()) {
353             // ETWS messages.
354             // Turn on/off emergency notifications is the only way to turn on/off ETWS messages.
355             return emergencyAlertEnabled;
356 
357         }
358 
359         if (message.isCmasMessage()) {
360             switch (message.getCmasMessageClass()) {
361                 case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
362                     return emergencyAlertEnabled &&
363                             PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
364                             CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
365 
366                 case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
367                     return emergencyAlertEnabled &&
368                             PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
369                             CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
370 
371                 case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
372                     return emergencyAlertEnabled &&
373                             PreferenceManager.getDefaultSharedPreferences(this)
374                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
375 
376                 case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
377                 case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
378                 case SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE:
379                     return emergencyAlertEnabled &&
380                             !forceDisableEtwsCmasTest &&
381                             PreferenceManager.getDefaultSharedPreferences(this)
382                                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS,
383                                             false);
384                 default:
385                     return true;    // presidential-level CMAS alerts are always enabled
386             }
387         }
388 
389         if (message.getServiceCategory() == 50) {
390             // save latest area info broadcast for Settings display and send as broadcast
391             CellBroadcastReceiverApp.setLatestAreaInfo(message);
392             Intent intent = new Intent(CB_AREA_INFO_RECEIVED_ACTION);
393             intent.putExtra("message", message);
394             // Send broadcast twice, once for apps that have PRIVILEGED permission and once
395             // for those that have the runtime one
396             sendBroadcastAsUser(intent, UserHandle.ALL,
397                     android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
398             sendBroadcastAsUser(intent, UserHandle.ALL,
399                     android.Manifest.permission.READ_PHONE_STATE);
400             return false;   // area info broadcasts are displayed in Settings status screen
401         }
402 
403         return true;    // other broadcast messages are always enabled
404     }
405 
406     /**
407      * Display a full-screen alert message for emergency alerts.
408      * @param message the alert to display
409      */
openEmergencyAlertNotification(CellBroadcastMessage message)410     private void openEmergencyAlertNotification(CellBroadcastMessage message) {
411         // Acquire a CPU wake lock until the alert dialog and audio start playing.
412         CellBroadcastAlertWakeLock.acquireScreenCpuWakeLock(this);
413 
414         // Close dialogs and window shade
415         Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
416         sendBroadcast(closeDialogs);
417 
418         // start audio/vibration/speech service for emergency alerts
419         Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
420         audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
421         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
422 
423         ToneType toneType = ToneType.CMAS_DEFAULT;
424         if (message.isEtwsMessage()) {
425             // For ETWS, always vibrate, even in silent mode.
426             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA, true);
427             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_ETWS_VIBRATE_EXTRA, true);
428             toneType = ToneType.ETWS_DEFAULT;
429 
430             if (message.getEtwsWarningInfo() != null) {
431                 int warningType = message.getEtwsWarningInfo().getWarningType();
432 
433                 switch (warningType) {
434                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE:
435                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
436                         toneType = ToneType.EARTHQUAKE;
437                         break;
438                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI:
439                         toneType = ToneType.TSUNAMI;
440                         break;
441                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY:
442                         toneType = ToneType.OTHER;
443                         break;
444                 }
445             }
446         } else {
447             // For other alerts, vibration can be disabled in app settings.
448             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATE_EXTRA,
449                     prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true));
450             int channel = message.getServiceCategory();
451             ArrayList<CellBroadcastChannelRange> ranges= CellBroadcastOtherChannelsManager.
452                     getInstance().getCellBroadcastChannelRanges(getApplicationContext(),
453                     message.getSubId());
454             if (ranges != null) {
455                 for (CellBroadcastChannelRange range : ranges) {
456                     if (channel >= range.mStartId && channel <= range.mEndId) {
457                         toneType = range.mToneType;
458                         break;
459                     }
460                 }
461             }
462         }
463         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE, toneType);
464 
465         String messageBody = message.getMessageBody();
466 
467         if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) {
468             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
469 
470             String preferredLanguage = message.getLanguageCode();
471             String defaultLanguage = null;
472             if (message.isEtwsMessage()) {
473                 // Only do TTS for ETWS secondary message.
474                 // There is no text in ETWS primary message. When we construct the ETWS primary
475                 // message, we hardcode "ETWS" as the body hence we don't want to speak that out
476                 // here.
477 
478                 // Also in many cases we see the secondary message comes few milliseconds after
479                 // the primary one. If we play TTS for the primary one, It will be overwritten by
480                 // the secondary one immediately anyway.
481                 if (!message.getEtwsWarningInfo().isPrimary()) {
482                     // Since only Japanese carriers are using ETWS, if there is no language
483                     // specified in the ETWS message, we'll use Japanese as the default language.
484                     defaultLanguage = "ja";
485                 }
486             } else {
487                 // If there is no language specified in the CMAS message, use device's
488                 // default language.
489                 defaultLanguage = Locale.getDefault().getLanguage();
490             }
491 
492             Log.d(TAG, "Preferred language = " + preferredLanguage +
493                     ", Default language = " + defaultLanguage);
494             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE,
495                     preferredLanguage);
496             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE,
497                     defaultLanguage);
498         }
499         startService(audioIntent);
500 
501         ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1);
502         messageList.add(message);
503 
504         Intent alertDialogIntent = createDisplayMessageIntent(this, CellBroadcastAlertDialog.class,
505                 messageList);
506         alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
507         startActivity(alertDialogIntent);
508     }
509 
510     /**
511      * Add the new alert to the notification bar (non-emergency alerts), or launch a
512      * high-priority immediate intent for emergency alerts.
513      * @param message the alert to display
514      */
addToNotificationBar(CellBroadcastMessage message, ArrayList<CellBroadcastMessage> messageList, Context context, boolean fromSaveState)515     static void addToNotificationBar(CellBroadcastMessage message,
516                                      ArrayList<CellBroadcastMessage> messageList, Context context,
517                                      boolean fromSaveState) {
518         int channelTitleId = CellBroadcastResources.getDialogTitleResource(context, message);
519         CharSequence channelName = context.getText(channelTitleId);
520         String messageBody = message.getMessageBody();
521 
522         // Create intent to show the new messages when user selects the notification.
523         Intent intent = createDisplayMessageIntent(context, CellBroadcastAlertDialog.class,
524                 messageList);
525 
526         intent.putExtra(CellBroadcastAlertDialog.FROM_NOTIFICATION_EXTRA, true);
527         intent.putExtra(CellBroadcastAlertDialog.FROM_SAVE_STATE_NOTIFICATION_EXTRA, fromSaveState);
528 
529         PendingIntent pi = PendingIntent.getActivity(context, NOTIFICATION_ID, intent,
530                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
531 
532         // use default sound/vibration/lights for non-emergency broadcasts
533         Notification.Builder builder = new Notification.Builder(context)
534                 .setSmallIcon(R.drawable.ic_notify_alert)
535                 .setTicker(channelName)
536                 .setWhen(System.currentTimeMillis())
537                 .setContentIntent(pi)
538                 .setCategory(Notification.CATEGORY_SYSTEM)
539                 .setPriority(Notification.PRIORITY_HIGH)
540                 .setColor(context.getResources().getColor(R.color.notification_color))
541                 .setVisibility(Notification.VISIBILITY_PUBLIC)
542                 .setDefaults(Notification.DEFAULT_ALL);
543 
544         builder.setDefaults(Notification.DEFAULT_ALL);
545 
546         // increment unread alert count (decremented when user dismisses alert dialog)
547         int unreadCount = messageList.size();
548         if (unreadCount > 1) {
549             // use generic count of unread broadcasts if more than one unread
550             builder.setContentTitle(context.getString(R.string.notification_multiple_title));
551             builder.setContentText(context.getString(R.string.notification_multiple, unreadCount));
552         } else {
553             builder.setContentTitle(channelName).setContentText(messageBody);
554         }
555 
556         NotificationManager notificationManager = NotificationManager.from(context);
557 
558         notificationManager.notify(NOTIFICATION_ID, builder.build());
559     }
560 
createDisplayMessageIntent(Context context, Class intentClass, ArrayList<CellBroadcastMessage> messageList)561     static Intent createDisplayMessageIntent(Context context, Class intentClass,
562             ArrayList<CellBroadcastMessage> messageList) {
563         // Trigger the list activity to fire up a dialog that shows the received messages
564         Intent intent = new Intent(context, intentClass);
565         intent.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, messageList);
566         return intent;
567     }
568 
569     @VisibleForTesting
570     @Override
onBind(Intent intent)571     public IBinder onBind(Intent intent) {
572         return new LocalBinder();
573     }
574 
575     @VisibleForTesting
576     class LocalBinder extends Binder {
getService()577         public CellBroadcastAlertService getService() {
578             return CellBroadcastAlertService.this;
579         }
580     }
581 
582     /**
583      * Check if the cell broadcast message is an emergency message or not
584      * @param context Device context
585      * @param cbm Cell broadcast message
586      * @return True if the message is an emergency message, otherwise false.
587      */
isEmergencyMessage(Context context, CellBroadcastMessage cbm)588     public static boolean isEmergencyMessage(Context context, CellBroadcastMessage cbm) {
589         boolean isEmergency = false;
590 
591         if (cbm == null) {
592             return false;
593         }
594 
595         int id = cbm.getServiceCategory();
596         int subId = cbm.getSubId();
597 
598         if (cbm.isEmergencyAlertMessage()) {
599             isEmergency = true;
600         } else {
601             ArrayList<CellBroadcastChannelRange> ranges = CellBroadcastOtherChannelsManager.
602                     getInstance().getCellBroadcastChannelRanges(context, subId);
603 
604             if (ranges != null) {
605                 for (CellBroadcastChannelRange range : ranges) {
606                     if (range.mStartId <= id && range.mEndId >= id) {
607                         isEmergency = range.mIsEmergency;
608                         break;
609                     }
610                 }
611             }
612         }
613 
614         Log.d(TAG, "isEmergencyMessage: " + isEmergency + ", subId = " + subId + ", " +
615                 "message id = " + id);
616         return isEmergency;
617     }
618 }
619