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