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