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