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