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.telephony.SmsCbMessage.MESSAGE_FORMAT_3GPP;
20 import static android.telephony.SmsCbMessage.MESSAGE_FORMAT_3GPP2;
21 
22 import android.annotation.NonNull;
23 import android.app.ActivityManager;
24 import android.app.Notification;
25 import android.app.NotificationChannel;
26 import android.app.NotificationManager;
27 import android.app.PendingIntent;
28 import android.app.Service;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.SharedPreferences;
33 import android.content.pm.PackageManager;
34 import android.content.res.Resources;
35 import android.media.AudioAttributes;
36 import android.media.AudioManager;
37 import android.net.Uri;
38 import android.os.Binder;
39 import android.os.Bundle;
40 import android.os.Handler;
41 import android.os.IBinder;
42 import android.os.Looper;
43 import android.os.SystemProperties;
44 import android.os.UserHandle;
45 import android.preference.PreferenceManager;
46 import android.provider.Telephony;
47 import android.service.notification.StatusBarNotification;
48 import android.telephony.PhoneStateListener;
49 import android.telephony.SmsCbEtwsInfo;
50 import android.telephony.SmsCbMessage;
51 import android.telephony.SubscriptionManager;
52 import android.telephony.TelephonyManager;
53 import android.text.TextUtils;
54 import android.util.Log;
55 
56 import com.android.cellbroadcastreceiver.CellBroadcastChannelManager.CellBroadcastChannelRange;
57 import com.android.cellbroadcastservice.CellBroadcastStatsLog;
58 import com.android.internal.annotations.VisibleForTesting;
59 
60 import java.util.ArrayList;
61 import java.util.Locale;
62 
63 /**
64  * This service manages the display and animation of broadcast messages.
65  * Emergency messages display with a flashing animated exclamation mark icon,
66  * and an alert tone is played when the alert is first shown to the user
67  * (but not when the user views a previously received broadcast).
68  */
69 public class CellBroadcastAlertService extends Service
70         implements AudioManager.OnAudioFocusChangeListener {
71     private static final String TAG = "CBAlertService";
72 
73     /** Intent action to display alert dialog/notification, after verifying the alert is new. */
74     @VisibleForTesting
75     public static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT";
76 
77     /** Identifier for getExtra() when adding this object to an Intent. */
78     public static final String SMS_CB_MESSAGE_EXTRA =
79             "com.android.cellbroadcastreceiver.SMS_CB_MESSAGE";
80 
81     /** Use the same notification ID for non-emergency alerts. */
82     @VisibleForTesting
83     public static final int NOTIFICATION_ID = 1;
84 
85     /**
86      * Notification channel containing for non-emergency alerts.
87      */
88     static final String NOTIFICATION_CHANNEL_NON_EMERGENCY_ALERTS = "broadcastMessagesNonEmergency";
89 
90     /**
91      * Notification channel for emergency alerts. This is used when users sneak out of the
92      * noisy pop-up for a real emergency and get a notification due to not officially acknowledged
93      * the alert and want to refer it back later.
94      */
95     static final String NOTIFICATION_CHANNEL_EMERGENCY_ALERTS = "broadcastMessages";
96 
97     /**
98      * Notification channel for emergency alerts during voice call. This is used when users in a
99      * voice call, emergency alert will be displayed in a notification format rather than playing
100      * alert tone.
101      */
102     static final String NOTIFICATION_CHANNEL_EMERGENCY_ALERTS_IN_VOICECALL =
103         "broadcastMessagesInVoiceCall";
104 
105     /** Intent extra for passing a SmsCbMessage */
106     private static final String EXTRA_MESSAGE = "message";
107 
108     /**
109      * Key for accessing message filter from SystemProperties. For testing use.
110      */
111     private static final String MESSAGE_FILTER_PROPERTY_KEY =
112             "persist.cellbroadcast.message_filter";
113 
114     private Context mContext;
115 
116     /**
117      * Alert type
118      */
119     public enum AlertType {
120         DEFAULT,
121         ETWS_DEFAULT,
122         ETWS_EARTHQUAKE,
123         ETWS_TSUNAMI,
124         TEST,
125         AREA,
126         INFO,
127         OTHER
128     }
129 
130     private TelephonyManager mTelephonyManager;
131     private AudioManager mAudioManager;
132 
133     /**
134      * Do not preempt active voice call, instead post notifications and play the ringtone/vibrate
135      * when the voicecall finish
136      */
137     private static boolean sRemindAfterCallFinish = false;
138 
139 
140     @Override
onStartCommand(Intent intent, int flags, int startId)141     public int onStartCommand(Intent intent, int flags, int startId) {
142         mContext = getApplicationContext();
143         String action = intent.getAction();
144         Log.d(TAG, "onStartCommand: " + action);
145         if (Telephony.Sms.Intents.ACTION_SMS_EMERGENCY_CB_RECEIVED.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             if (UserHandle.myUserId() == ((ActivityManager) getSystemService(
150                     Context.ACTIVITY_SERVICE)).getCurrentUser()) {
151                 showNewAlert(intent);
152             } else {
153                 Log.d(TAG, "Not active user, ignore the alert display");
154             }
155         } else {
156             Log.e(TAG, "Unrecognized intent action: " + action);
157         }
158         return START_NOT_STICKY;
159     }
160 
161     @Override
onCreate()162     public void onCreate() {
163         mTelephonyManager = (TelephonyManager)
164                 getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
165         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
166         mAudioManager = (AudioManager)
167             getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
168     }
169 
170     @Override
onDestroy()171     public void onDestroy() {
172         // Stop listening for incoming calls.
173         mTelephonyManager.listen(mPhoneStateListener, 0);
174 
175     }
176 
177     /**
178      * Check if we should display the received cell broadcast message.
179      *
180      * @param message Cell broadcast message
181      * @return True if the message should be displayed to the user
182      */
shouldDisplayMessage(SmsCbMessage message)183     private boolean shouldDisplayMessage(SmsCbMessage message) {
184         TelephonyManager tm = ((TelephonyManager) mContext.getSystemService(
185                 Context.TELEPHONY_SERVICE)).createForSubscriptionId(message.getSubscriptionId());
186         if (tm.getEmergencyCallbackMode() && CellBroadcastSettings.getResources(
187                 mContext, message.getSubscriptionId()).getBoolean(R.bool.ignore_messages_in_ecbm)) {
188             // Ignore the message in ECBM.
189             // It is for LTE only mode. For 1xRTT, incoming pages should be ignored in the modem.
190             Log.d(TAG, "ignoring alert of type " + message.getServiceCategory() + " in ECBM");
191             return false;
192         }
193         // Check if the channel is enabled by the user or configuration.
194         if (!isChannelEnabled(message)) {
195             Log.d(TAG, "ignoring alert of type " + message.getServiceCategory()
196                     + " by user preference");
197             return false;
198         }
199 
200         // Check if message body is empty
201         String msgBody = message.getMessageBody();
202         if (msgBody == null || msgBody.length() == 0) {
203             Log.e(TAG, "Empty content or Unsupported charset");
204             return false;
205         }
206 
207         // Check if we need to perform language filtering.
208         CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(mContext,
209                 message.getSubscriptionId());
210         CellBroadcastChannelRange range = channelManager
211                 .getCellBroadcastChannelRangeFromMessage(message);
212         String messageLanguage = message.getLanguageCode();
213         if (range != null && range.mFilterLanguage) {
214             // language filtering based on CBR second language settings
215             final String secondLanguageCode =  CellBroadcastSettings.getResources(mContext,
216                     SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
217                     .getString(R.string.emergency_alert_second_language_code);
218             if (!secondLanguageCode.isEmpty()) {
219                 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
220                 boolean receiveInSecondLanguage = prefs.getBoolean(
221                         CellBroadcastSettings.KEY_RECEIVE_CMAS_IN_SECOND_LANGUAGE, false);
222                 // For DCS values that bit 6 is 1 and bit 7 is 0, language field is not defined so
223                 // ap receives it as null value and so alert is not shown to the user.
224                 // bypass language filter in this case.
225                 if (!TextUtils.isEmpty(messageLanguage)
226                         && !secondLanguageCode.equalsIgnoreCase(messageLanguage)) {
227                     Log.w(TAG, "Ignoring message in the unspecified second language:"
228                             + messageLanguage);
229                     return false;
230                 } else if (!receiveInSecondLanguage) {
231                     Log.d(TAG, "Ignoring message in second language because setting is off");
232                     return false;
233                 }
234             } else {
235                 // language filtering based on device language settings.
236                 String deviceLanguage = Locale.getDefault().getLanguage();
237                 // Apply If the message's language does not match device's message, we don't
238                 // display the message.
239                 if (!TextUtils.isEmpty(messageLanguage)
240                         && !messageLanguage.equalsIgnoreCase(deviceLanguage)) {
241                     Log.d(TAG, "ignoring the alert due to language mismatch. Message lang="
242                             + messageLanguage + ", device lang=" + deviceLanguage);
243                     return false;
244                 }
245             }
246         }
247 
248         // Check for custom filtering
249         String messageFilters = SystemProperties.get(MESSAGE_FILTER_PROPERTY_KEY, "");
250         if (!TextUtils.isEmpty(messageFilters)) {
251             String[] filters = messageFilters.split(",");
252             for (String filter : filters) {
253                 if (!TextUtils.isEmpty(filter)) {
254                     if (message.getMessageBody().toLowerCase().contains(filter)) {
255                         Log.i(TAG, "Skipped message due to filter: " + filter);
256                         return false;
257                     }
258                 }
259             }
260         }
261 
262         return true;
263     }
264 
handleCellBroadcastIntent(Intent intent)265     private void handleCellBroadcastIntent(Intent intent) {
266         Bundle extras = intent.getExtras();
267         if (extras == null) {
268             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no extras!");
269             return;
270         }
271 
272         SmsCbMessage message = (SmsCbMessage) extras.get(EXTRA_MESSAGE);
273 
274         if (message == null) {
275             Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra");
276             return;
277         }
278 
279         if (message.getMessageFormat() == MESSAGE_FORMAT_3GPP) {
280             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_REPORTED,
281                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_REPORTED__TYPE__GSM,
282                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_REPORTED__SOURCE__CB_RECEIVER_APP);
283         } else if (message.getMessageFormat() == MESSAGE_FORMAT_3GPP2) {
284             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_REPORTED,
285                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_REPORTED__TYPE__CDMA,
286                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_REPORTED__SOURCE__CB_RECEIVER_APP);
287         }
288 
289         if (!shouldDisplayMessage(message)) {
290             return;
291         }
292 
293         final Intent alertIntent = new Intent(SHOW_NEW_ALERT_ACTION);
294         alertIntent.setClass(this, CellBroadcastAlertService.class);
295         alertIntent.putExtra(EXTRA_MESSAGE, message);
296 
297         // write to database on a background thread
298         new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver())
299                 .execute((CellBroadcastContentProvider.CellBroadcastOperation) provider -> {
300                     if (provider.insertNewBroadcast(message)) {
301                         // new message, show the alert or notification on UI thread
302                         startService(alertIntent);
303                         // mark the message as displayed to the user.
304                         markMessageDisplayed(message);
305                         if (CellBroadcastSettings.getResources(mContext,
306                                 message.getSubscriptionId())
307                                 .getBoolean(R.bool.enable_write_alerts_to_sms_inbox)) {
308                             // TODO: Should not create the instance of channel manager everywhere.
309                             CellBroadcastChannelManager channelManager =
310                                     new CellBroadcastChannelManager(mContext,
311                                             message.getSubscriptionId());
312                             CellBroadcastChannelRange range = channelManager
313                                     .getCellBroadcastChannelRangeFromMessage(message);
314                             if (CellBroadcastReceiver.isTestingMode(getApplicationContext())
315                                     || (range != null && range.mWriteToSmsInbox)) {
316                                 writeMessageToSmsInbox(message);
317                             }
318                         }
319                         return true;
320                     } else {
321                         return false;
322                     }
323                 });
324     }
325 
326     /**
327      * Mark the message as displayed in cell broadcast service's database.
328      *
329      * @param message The cell broadcast message.
330      */
markMessageDisplayed(SmsCbMessage message)331     private void markMessageDisplayed(SmsCbMessage message) {
332         ContentValues cv = new ContentValues();
333         cv.put(Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 1);
334         mContext.getContentResolver().update(Telephony.CellBroadcasts.CONTENT_URI, cv,
335                 Telephony.CellBroadcasts.RECEIVED_TIME + "=?",
336                 new String[] {Long.toString(message.getReceivedTime())});
337     }
338 
showNewAlert(Intent intent)339     private void showNewAlert(Intent intent) {
340         Bundle extras = intent.getExtras();
341         if (extras == null) {
342             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no extras!");
343             return;
344         }
345 
346         SmsCbMessage cbm = intent.getParcelableExtra(EXTRA_MESSAGE);
347 
348         if (cbm == null) {
349             Log.e(TAG, "received SHOW_NEW_ALERT_ACTION with no message extra");
350             return;
351         }
352 
353         if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE
354                 && CellBroadcastSettings.getResources(mContext, cbm.getSubscriptionId())
355                 .getBoolean(R.bool.enable_alert_handling_during_call)) {
356             Log.d(TAG, "CMAS received in dialing/during voicecall.");
357             sRemindAfterCallFinish = true;
358         }
359 
360         // Either shown the dialog, adding it to notification (non emergency, or delayed emergency),
361         CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(
362                 mContext, cbm.getSubscriptionId());
363         if (channelManager.isEmergencyMessage(cbm) && !sRemindAfterCallFinish) {
364             // start alert sound / vibration / TTS and display full-screen alert
365             openEmergencyAlertNotification(cbm);
366         } else {
367             // add notification to the bar by passing the list of unread non-emergency
368             // cell broadcast messages
369             ArrayList<SmsCbMessage> messageList = CellBroadcastReceiverApp
370                     .addNewMessageToList(cbm);
371             addToNotificationBar(cbm, messageList, this, false);
372         }
373     }
374 
375     /**
376      * Check if the message's channel is enabled on the device.
377      *
378      * @param message the message to check
379      * @return true if the channel is enabled on the device, otherwise false.
380      */
isChannelEnabled(SmsCbMessage message)381     private boolean isChannelEnabled(SmsCbMessage message) {
382 
383         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
384         // Check if all emergency alerts are disabled.
385         boolean emergencyAlertEnabled =
386                 prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true);
387 
388         SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo();
389         if (etwsInfo != null
390                 && etwsInfo.getWarningType() == SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE) {
391             return emergencyAlertEnabled
392                     && CellBroadcastSettings.isTestAlertsToggleVisible(getApplicationContext())
393                     && PreferenceManager.getDefaultSharedPreferences(this)
394                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, false);
395         }
396 
397         if (message.isEtwsMessage()) {
398             // ETWS messages.
399             // Turn on/off emergency notifications is the only way to turn on/off ETWS messages.
400             return emergencyAlertEnabled;
401 
402         }
403 
404         int channel = message.getServiceCategory();
405 
406         // Check if the messages are on additional channels enabled by the resource config.
407         // If those channels are enabled by the carrier, but the device is actually roaming, we
408         // should not allow the messages.
409         CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(
410                 mContext, message.getSubscriptionId());
411         ArrayList<CellBroadcastChannelRange> ranges = channelManager.getCellBroadcastChannelRanges(
412                 R.array.additional_cbs_channels_strings);
413 
414         for (CellBroadcastChannelRange range : ranges) {
415             if (range.mStartId <= channel && range.mEndId >= channel) {
416                 // Check if the channel is within the scope. If not, ignore the alert message.
417                 if (!channelManager.checkScope(range.mScope)) {
418                     Log.d(TAG, "The range [" + range.mStartId + "-" + range.mEndId
419                             + "] is not within the scope. mScope = " + range.mScope);
420                     return false;
421                 }
422 
423                 if (range.mAlertType == AlertType.TEST) {
424                     return emergencyAlertEnabled
425                             && CellBroadcastSettings.isTestAlertsToggleVisible(
426                                     getApplicationContext())
427                             && PreferenceManager.getDefaultSharedPreferences(this)
428                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS,
429                                     false);
430                 }
431 
432                 return emergencyAlertEnabled;
433             }
434         }
435 
436         if (channelManager.checkCellBroadcastChannelRange(channel,
437                 R.array.emergency_alerts_channels_range_strings)) {
438             return emergencyAlertEnabled
439                     && PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
440                             CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS, true);
441         }
442         // CMAS warning types
443         if (channelManager.checkCellBroadcastChannelRange(channel,
444                 R.array.cmas_presidential_alerts_channels_range_strings)) {
445             // always enabled
446             return true;
447         }
448         if (channelManager.checkCellBroadcastChannelRange(channel,
449                 R.array.cmas_alert_extreme_channels_range_strings)) {
450             return emergencyAlertEnabled
451                     && PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
452                             CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true);
453         }
454         if (channelManager.checkCellBroadcastChannelRange(channel,
455                 R.array.cmas_alerts_severe_range_strings)) {
456             return emergencyAlertEnabled
457                     && PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
458                             CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true);
459         }
460         if (channelManager.checkCellBroadcastChannelRange(channel,
461                 R.array.cmas_amber_alerts_channels_range_strings)) {
462             return emergencyAlertEnabled
463                     && PreferenceManager.getDefaultSharedPreferences(this)
464                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true);
465         }
466 
467         if (channelManager.checkCellBroadcastChannelRange(channel,
468                 R.array.exercise_alert_range_strings)
469                 && getResources().getBoolean(R.bool.always_enable_exercise_alert)) {
470             return true;
471         }
472 
473         if (channelManager.checkCellBroadcastChannelRange(channel,
474                 R.array.required_monthly_test_range_strings)
475                 || channelManager.checkCellBroadcastChannelRange(channel,
476                 R.array.exercise_alert_range_strings)
477                 || channelManager.checkCellBroadcastChannelRange(channel,
478                 R.array.operator_defined_alert_range_strings)) {
479             return emergencyAlertEnabled
480                     && CellBroadcastSettings.isTestAlertsToggleVisible(getApplicationContext())
481                     && PreferenceManager.getDefaultSharedPreferences(this)
482                             .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS,
483                                     false);
484         }
485 
486         if (channelManager.checkCellBroadcastChannelRange(channel,
487                 R.array.public_safety_messages_channels_range_strings)) {
488             return emergencyAlertEnabled
489                     && PreferenceManager.getDefaultSharedPreferences(this)
490                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_PUBLIC_SAFETY_MESSAGES,
491                             true);
492         }
493 
494         if (channelManager.checkCellBroadcastChannelRange(channel,
495                 R.array.state_local_test_alert_range_strings)) {
496             return emergencyAlertEnabled
497                     && PreferenceManager.getDefaultSharedPreferences(this)
498                     .getBoolean(CellBroadcastSettings.KEY_ENABLE_STATE_LOCAL_TEST_ALERTS,
499                             false);
500         }
501 
502         return true;
503     }
504 
505     /**
506      * Display an alert message for emergency alerts.
507      * @param message the alert to display
508      */
openEmergencyAlertNotification(SmsCbMessage message)509     private void openEmergencyAlertNotification(SmsCbMessage message) {
510         // Close dialogs and window shade
511         Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
512         sendBroadcast(closeDialogs);
513 
514         // start audio/vibration/speech service for emergency alerts
515         Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class);
516         audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
517         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
518 
519         CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(
520                 mContext, message.getSubscriptionId());
521 
522         AlertType alertType = AlertType.DEFAULT;
523         if (message.isEtwsMessage()) {
524             alertType = AlertType.ETWS_DEFAULT;
525 
526             if (message.getEtwsWarningInfo() != null) {
527                 int warningType = message.getEtwsWarningInfo().getWarningType();
528 
529                 switch (warningType) {
530                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE:
531                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
532                         alertType = AlertType.ETWS_EARTHQUAKE;
533                         break;
534                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI:
535                         alertType = AlertType.ETWS_TSUNAMI;
536                         break;
537                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE:
538                         alertType = AlertType.TEST;
539                         break;
540                     case SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY:
541                         alertType = AlertType.OTHER;
542                         break;
543                 }
544             }
545         } else {
546             int channel = message.getServiceCategory();
547             ArrayList<CellBroadcastChannelRange> ranges = channelManager
548                     .getAllCellBroadcastChannelRanges();
549             for (CellBroadcastChannelRange range : ranges) {
550                 if (channel >= range.mStartId && channel <= range.mEndId) {
551                     alertType = range.mAlertType;
552                     break;
553                 }
554             }
555         }
556         CellBroadcastChannelRange range = channelManager
557                 .getCellBroadcastChannelRangeFromMessage(message);
558         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE, alertType);
559         audioIntent.putExtra(
560                 CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATION_PATTERN_EXTRA,
561                 (range != null)
562                         ? range.mVibrationPattern
563                         : CellBroadcastSettings.getResources(mContext, message.getSubscriptionId())
564                         .getIntArray(R.array.default_vibration_pattern));
565 
566         if (prefs.getBoolean(CellBroadcastSettings.KEY_OVERRIDE_DND, false)
567                 || (range != null && range.mOverrideDnd)) {
568             audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_OVERRIDE_DND_EXTRA, true);
569         }
570 
571         String messageBody = message.getMessageBody();
572 
573         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody);
574 
575         String language = message.getLanguageCode();
576 
577         Log.d(TAG, "Message language = " + language);
578         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE,
579                 language);
580 
581         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_SUB_INDEX,
582                 message.getSubscriptionId());
583         audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION,
584                 (range != null) ? range.mAlertDuration : -1);
585         startService(audioIntent);
586 
587         ArrayList<SmsCbMessage> messageList = new ArrayList<>();
588         messageList.add(message);
589 
590         // For FEATURE_WATCH, the dialog doesn't make sense from a UI/UX perspective
591         if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
592             addToNotificationBar(message, messageList, this, false);
593         } else {
594             Intent alertDialogIntent = createDisplayMessageIntent(this,
595                     CellBroadcastAlertDialog.class, messageList);
596             alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
597             startActivity(alertDialogIntent);
598         }
599 
600     }
601 
602     /**
603      * Add the new alert to the notification bar (non-emergency alerts), or launch a
604      * high-priority immediate intent for emergency alerts.
605      * @param message the alert to display
606      */
addToNotificationBar(SmsCbMessage message, ArrayList<SmsCbMessage> messageList, Context context, boolean fromSaveState)607     static void addToNotificationBar(SmsCbMessage message,
608                                      ArrayList<SmsCbMessage> messageList, Context context,
609                                      boolean fromSaveState) {
610         Resources res = CellBroadcastSettings.getResources(context, message.getSubscriptionId());
611         int channelTitleId = CellBroadcastResources.getDialogTitleResource(context, message);
612         CharSequence channelName = context.getText(channelTitleId);
613         String messageBody = message.getMessageBody();
614         final NotificationManager notificationManager =
615                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
616         createNotificationChannels(context);
617 
618         // Create intent to show the new messages when user selects the notification.
619         Intent intent;
620         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
621             // For FEATURE_WATCH we want to mark as read
622             intent = createMarkAsReadIntent(context, message.getReceivedTime());
623         } else {
624             // For anything else we handle it normally
625             intent = createDisplayMessageIntent(context, CellBroadcastAlertDialog.class,
626                     messageList);
627         }
628 
629         intent.putExtra(CellBroadcastAlertDialog.FROM_NOTIFICATION_EXTRA, true);
630         intent.putExtra(CellBroadcastAlertDialog.FROM_SAVE_STATE_NOTIFICATION_EXTRA, fromSaveState);
631 
632         PendingIntent pi;
633         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
634             pi = PendingIntent.getBroadcast(context, 0, intent, 0);
635         } else {
636             pi = PendingIntent.getActivity(context, NOTIFICATION_ID, intent,
637                     PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
638         }
639         CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(
640                 context, message.getSubscriptionId());
641 
642         String channelId = channelManager.isEmergencyMessage(message)
643                 ? NOTIFICATION_CHANNEL_EMERGENCY_ALERTS : NOTIFICATION_CHANNEL_NON_EMERGENCY_ALERTS;
644         if (channelId == NOTIFICATION_CHANNEL_EMERGENCY_ALERTS && sRemindAfterCallFinish) {
645             channelId = NOTIFICATION_CHANNEL_EMERGENCY_ALERTS_IN_VOICECALL;
646         }
647 
648         boolean nonSwipeableNotification = message.isEmergencyMessage()
649                 && CellBroadcastSettings.getResources(context, message.getSubscriptionId())
650                 .getBoolean(R.bool.non_swipeable_notification) || sRemindAfterCallFinish;
651 
652         // use default sound/vibration/lights for non-emergency broadcasts
653         Notification.Builder builder =
654                 new Notification.Builder(context, channelId)
655                         .setSmallIcon(R.drawable.ic_warning_googred)
656                         .setTicker(channelName)
657                         .setWhen(System.currentTimeMillis())
658                         .setCategory(Notification.CATEGORY_SYSTEM)
659                         .setPriority(Notification.PRIORITY_HIGH)
660                         .setColor(res.getColor(R.color.notification_color))
661                         .setVisibility(Notification.VISIBILITY_PUBLIC)
662                         .setOngoing(nonSwipeableNotification);
663 
664         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
665             builder.setDeleteIntent(pi);
666             // FEATURE_WATCH/CWH devices see this as priority
667             builder.setVibrate(new long[]{0});
668         } else {
669             builder.setContentIntent(pi);
670             // This will break vibration on FEATURE_WATCH, so use it for anything else
671             builder.setDefaults(Notification.DEFAULT_ALL);
672         }
673 
674         // increment unread alert count (decremented when user dismisses alert dialog)
675         int unreadCount = messageList.size();
676         if (unreadCount > 1) {
677             // use generic count of unread broadcasts if more than one unread
678             builder.setContentTitle(context.getString(R.string.notification_multiple_title));
679             builder.setContentText(context.getString(R.string.notification_multiple, unreadCount));
680         } else {
681             builder.setContentTitle(channelName)
682                     .setContentText(messageBody)
683                     .setStyle(new Notification.BigTextStyle()
684                             .bigText(messageBody));
685         }
686 
687         notificationManager.notify(NOTIFICATION_ID, builder.build());
688 
689         // FEATURE_WATCH devices do not have global sounds for notifications; only vibrate.
690         // TW requires sounds for 911/919
691         // Emergency messages use a different audio playback and display path. Since we use
692         // addToNotification for the emergency display on FEATURE WATCH devices vs the
693         // Alert Dialog, it will call this and override the emergency audio tone.
694         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
695                 && !channelManager.isEmergencyMessage(message)) {
696             if (res.getBoolean(R.bool.watch_enable_non_emergency_audio)) {
697                 // start audio/vibration/speech service for non emergency alerts
698                 Intent audioIntent = new Intent(context, CellBroadcastAlertAudio.class);
699                 audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO);
700                 audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE,
701                         AlertType.OTHER);
702                 context.startService(audioIntent);
703             }
704         }
705 
706     }
707 
708     /**
709      * Creates the notification channel and registers it with NotificationManager. If a channel
710      * with the same ID is already registered, NotificationManager will ignore this call.
711      */
createNotificationChannels(Context context)712     static void createNotificationChannels(Context context) {
713         NotificationManager notificationManager =
714                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
715         notificationManager.createNotificationChannel(
716                 new NotificationChannel(
717                         NOTIFICATION_CHANNEL_EMERGENCY_ALERTS,
718                         context.getString(R.string.notification_channel_emergency_alerts),
719                         NotificationManager.IMPORTANCE_LOW));
720         final NotificationChannel nonEmergency = new NotificationChannel(
721                 NOTIFICATION_CHANNEL_NON_EMERGENCY_ALERTS,
722                 context.getString(R.string.notification_channel_broadcast_messages),
723                 NotificationManager.IMPORTANCE_DEFAULT);
724         nonEmergency.enableVibration(true);
725         notificationManager.createNotificationChannel(nonEmergency);
726 
727         final NotificationChannel emergencyAlertInVoiceCall = new NotificationChannel(
728             NOTIFICATION_CHANNEL_EMERGENCY_ALERTS_IN_VOICECALL,
729             context.getString(R.string.notification_channel_broadcast_messages_in_voicecall),
730             NotificationManager.IMPORTANCE_HIGH);
731         emergencyAlertInVoiceCall.enableVibration(true);
732         notificationManager.createNotificationChannel(emergencyAlertInVoiceCall);
733     }
734 
createDisplayMessageIntent(Context context, Class intentClass, ArrayList<SmsCbMessage> messageList)735     private static Intent createDisplayMessageIntent(Context context, Class intentClass,
736             ArrayList<SmsCbMessage> messageList) {
737         // Trigger the list activity to fire up a dialog that shows the received messages
738         Intent intent = new Intent(context, intentClass);
739         intent.putParcelableArrayListExtra(CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA,
740                 messageList);
741         return intent;
742     }
743 
744     /**
745      * Creates a delete intent that calls to the {@link CellBroadcastReceiver} in order to mark
746      * a message as read
747      *
748      * @param context context of the caller
749      * @param deliveryTime time the message was sent in order to mark as read
750      * @return delete intent to add to the pending intent
751      */
createMarkAsReadIntent(Context context, long deliveryTime)752     static Intent createMarkAsReadIntent(Context context, long deliveryTime) {
753         Intent deleteIntent = new Intent(context, CellBroadcastReceiver.class);
754         deleteIntent.setAction(CellBroadcastReceiver.ACTION_MARK_AS_READ);
755         deleteIntent.putExtra(CellBroadcastReceiver.EXTRA_DELIVERY_TIME, deliveryTime);
756         return deleteIntent;
757     }
758 
759     @VisibleForTesting
760     @Override
onBind(Intent intent)761     public IBinder onBind(Intent intent) {
762         return new LocalBinder();
763     }
764 
765     @VisibleForTesting
766     class LocalBinder extends Binder {
getService()767         public CellBroadcastAlertService getService() {
768             return CellBroadcastAlertService.this;
769         }
770     }
771 
772     @Override
onAudioFocusChange(int focusChange)773     public void onAudioFocusChange(int focusChange) {
774         if(focusChange == AudioManager.AUDIOFOCUS_GAIN) {
775             Log.d(TAG, "audio focus released from voice call, play pending alert if needed");
776             mAudioManager.abandonAudioFocus(this);
777             playPendingAlert();
778         }
779     }
780 
781     /**
782      * Remove previous unread notifications and play stored unread
783      * emergency messages after voice call finish.
784      */
785     private final PhoneStateListener mPhoneStateListener = new PhoneStateListener(
786         new Handler(Looper.getMainLooper())::post) {
787         @Override
788         public void onCallStateChanged(int state, String incomingNumber) {
789 
790             switch (state) {
791                 case TelephonyManager.CALL_STATE_IDLE:
792                     Log.d(TAG, "onCallStateChanged: CALL_STATE_IDLE");
793                     // check if audio focus was released by voice call. This is to avoid possible
794                     // race conditions that voice call did not release audio focus while alert is
795                     // playing at the same time (out-of-rhythm)
796                     if (mAudioManager == null) {
797                         mAudioManager = (AudioManager)
798                             getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
799                     }
800                     int audioFocusResult = mAudioManager.requestAudioFocus(
801                             CellBroadcastAlertService.this::onAudioFocusChange,
802                             new AudioAttributes.Builder().setLegacyStreamType(
803                                     AudioManager.STREAM_ALARM).build(),
804                             AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
805                             AudioManager.AUDIOFOCUS_FLAG_DELAY_OK);
806                     if (audioFocusResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
807                         Log.d(TAG, "audio focus released from voice call, "
808                                 + "play pending alert if needed");
809                         mAudioManager.abandonAudioFocus(
810                                 CellBroadcastAlertService.this::onAudioFocusChange);
811                         playPendingAlert();
812                     } else {
813                         Log.d(TAG, "wait for audio focus release after call");
814                     }
815                     break;
816 
817                 default:
818                     Log.d(TAG, "onCallStateChanged: other state = " + state);
819                     break;
820             }
821         }
822     };
823 
playPendingAlert()824     private void playPendingAlert() {
825         if (sRemindAfterCallFinish) {
826             sRemindAfterCallFinish = false;
827             NotificationManager notificationManager = (NotificationManager)
828                     getApplicationContext().getSystemService(
829                             Context.NOTIFICATION_SERVICE);
830 
831             StatusBarNotification[] notificationList =
832                     notificationManager.getActiveNotifications();
833 
834             if(notificationList != null && notificationList.length >0) {
835                 notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID);
836                 ArrayList<SmsCbMessage> newMessageList =
837                         CellBroadcastReceiverApp.getNewMessageList();
838 
839                 for (int i = 0; i < newMessageList.size(); i++) {
840                     openEmergencyAlertNotification(newMessageList.get(i));
841                 }
842             }
843             CellBroadcastReceiverApp.clearNewMessageList();
844         }
845     }
846 
847     /**
848      * Write displayed cellbroadcast messages to sms inbox
849      *
850      * @param message The cell broadcast message.
851      */
writeMessageToSmsInbox(@onNull SmsCbMessage message)852     private void writeMessageToSmsInbox(@NonNull SmsCbMessage message) {
853         // composing SMS
854         ContentValues cv = new ContentValues();
855         cv.put(Telephony.Sms.Inbox.BODY, message.getMessageBody());
856         cv.put(Telephony.Sms.Inbox.DATE, message.getReceivedTime());
857         cv.put(Telephony.Sms.Inbox.SUBSCRIPTION_ID, message.getSubscriptionId());
858         cv.put(Telephony.Sms.Inbox.SUBJECT, CellBroadcastResources.getDialogTitleResource(mContext,
859                 message));
860         cv.put(Telephony.Sms.Inbox.ADDRESS, mContext.getString(R.string.sms_cb_sender_name));
861         // store all cellbroadcast messages in the same thread.
862         cv.put(Telephony.Sms.Inbox.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext,
863                 mContext.getString(R.string.sms_cb_sender_name)));
864         Uri uri = mContext.getContentResolver().insert(Telephony.Sms.Inbox.CONTENT_URI, cv);
865         if (uri == null) {
866             Log.e(TAG, "writeMessageToSmsInbox: failed");
867         } else {
868             Log.d(TAG, "writeMessageToSmsInbox: succeed uri = " + uri);
869         }
870     }
871 }
872