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