• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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.cellbroadcastservice;
18 
19 import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
20 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
21 
22 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_AMBIGUOUS;
23 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND;
24 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_NO_COORDINATES;
25 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND;
26 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_SENT;
27 import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_CDMA_MESSAGE_TYPE_FROM_FWK;
28 
29 import android.annotation.NonNull;
30 import android.annotation.Nullable;
31 import android.app.Activity;
32 import android.content.BroadcastReceiver;
33 import android.content.ContentValues;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.PackageManager;
39 import android.content.pm.ResolveInfo;
40 import android.content.res.Resources;
41 import android.database.Cursor;
42 import android.location.Location;
43 import android.location.LocationListener;
44 import android.location.LocationManager;
45 import android.location.LocationRequest;
46 import android.net.Uri;
47 import android.os.Bundle;
48 import android.os.Handler;
49 import android.os.HandlerExecutor;
50 import android.os.Looper;
51 import android.os.Message;
52 import android.os.Process;
53 import android.os.SystemClock;
54 import android.os.SystemProperties;
55 import android.os.UserHandle;
56 import android.provider.Telephony;
57 import android.provider.Telephony.CellBroadcasts;
58 import android.telephony.CbGeoUtils.LatLng;
59 import android.telephony.CellBroadcastIntents;
60 import android.telephony.SmsCbMessage;
61 import android.telephony.SubscriptionManager;
62 import android.telephony.cdma.CdmaSmsCbProgramData;
63 import android.text.TextUtils;
64 import android.util.LocalLog;
65 import android.util.Log;
66 
67 import com.android.internal.annotations.VisibleForTesting;
68 
69 import java.io.File;
70 import java.io.FileDescriptor;
71 import java.io.PrintWriter;
72 import java.text.DateFormat;
73 import java.util.ArrayList;
74 import java.util.Arrays;
75 import java.util.HashMap;
76 import java.util.List;
77 import java.util.Map;
78 import java.util.Objects;
79 import java.util.concurrent.TimeUnit;
80 import java.util.stream.Collectors;
81 import java.util.stream.Stream;
82 
83 /**
84  * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast
85  * completes and our result receiver is called.
86  */
87 public class CellBroadcastHandler extends WakeLockStateMachine {
88     private static final String TAG = "CellBroadcastHandler";
89 
90     /**
91      * CellBroadcast apex name
92      */
93     private static final String CB_APEX_NAME = "com.android.cellbroadcast";
94 
95     /**
96      * CellBroadcast app platform name
97      */
98     private static final String CB_APP_PLATFORM_NAME = "CellBroadcastAppPlatform";
99 
100     /**
101      * Path where CB apex is mounted (/apex/com.android.cellbroadcast)
102      */
103     private static final String CB_APEX_PATH = new File("/apex", CB_APEX_NAME).getAbsolutePath();
104 
105     private static final String EXTRA_MESSAGE = "message";
106 
107     /**
108      * To disable cell broadcast duplicate detection for debugging purposes
109      * <code>adb shell am broadcast -a com.android.cellbroadcastservice.action.DUPLICATE_DETECTION
110      * --ez enable false</code>
111      *
112      * To enable cell broadcast duplicate detection for debugging purposes
113      * <code>adb shell am broadcast -a com.android.cellbroadcastservice.action.DUPLICATE_DETECTION
114      * --ez enable true</code>
115      */
116     private static final String ACTION_DUPLICATE_DETECTION =
117             "com.android.cellbroadcastservice.action.DUPLICATE_DETECTION";
118 
119     /**
120      * The extra for cell broadcast duplicate detection enable/disable
121      */
122     private static final String EXTRA_ENABLE = "enable";
123 
124     private final LocalLog mLocalLog = new LocalLog(100);
125 
126     private static final boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1;
127 
128     /** Uses to request the location update. */
129     private final LocationRequester mLocationRequester;
130 
131     /** Used to inject new calculators during unit testing */
132     @NonNull protected final CbSendMessageCalculatorFactory mCbSendMessageCalculatorFactory;
133 
134     /** Timestamp of last airplane mode on */
135     protected long mLastAirplaneModeTime = 0;
136 
137     /** Resource cache */
138     protected final Map<Integer, Resources> mResourcesCache = new HashMap<>();
139 
140     /** Whether performing duplicate detection or not. Note this is for debugging purposes only. */
141     private boolean mEnableDuplicateDetection = true;
142 
143     /**
144      * Service category equivalent map. The key is the GSM service category, the value is the CDMA
145      * service category.
146      */
147     private final Map<Integer, Integer> mServiceCategoryCrossRATMap;
148 
149     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
150         @Override
151         public void onReceive(Context context, Intent intent) {
152             switch (intent.getAction()) {
153                 case Intent.ACTION_AIRPLANE_MODE_CHANGED:
154                     boolean airplaneModeOn = intent.getBooleanExtra("state", false);
155                     if (airplaneModeOn) {
156                         mLastAirplaneModeTime = System.currentTimeMillis();
157                         log("Airplane mode on.");
158                     }
159                     break;
160                 case ACTION_DUPLICATE_DETECTION:
161                     mEnableDuplicateDetection = intent.getBooleanExtra(EXTRA_ENABLE,
162                             true);
163                     log("Duplicate detection " + (mEnableDuplicateDetection
164                             ? "enabled" : "disabled"));
165                     break;
166                 default:
167                     log("Unhandled broadcast " + intent.getAction());
168             }
169         }
170     };
171 
CellBroadcastHandler(Context context)172     private CellBroadcastHandler(Context context) {
173         this(CellBroadcastHandler.class.getSimpleName(), context, Looper.myLooper(),
174                 new CbSendMessageCalculatorFactory(), null);
175     }
176 
177     /**
178      * Allows tests to inject new calculators
179      */
180     @VisibleForTesting
181     public static class CbSendMessageCalculatorFactory {
CbSendMessageCalculatorFactory()182         public CbSendMessageCalculatorFactory() {
183         }
184 
185         /**
186          * Creates new calculator
187          * @param context context
188          * @param fences the geo fences to use in the calculator
189          * @return a new instance of the calculator
190          */
createNew(@onNull final Context context, @NonNull final List<android.telephony.CbGeoUtils.Geometry> fences)191         public CbSendMessageCalculator createNew(@NonNull final Context context,
192                 @NonNull final List<android.telephony.CbGeoUtils.Geometry> fences) {
193             return new CbSendMessageCalculator(context, fences);
194         }
195     }
196 
197     @VisibleForTesting
CellBroadcastHandler(String debugTag, Context context, Looper looper, @NonNull final CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory, @Nullable HandlerHelper handlerHelper)198     public CellBroadcastHandler(String debugTag, Context context, Looper looper,
199             @NonNull final CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory,
200             @Nullable HandlerHelper handlerHelper) {
201         super(debugTag, context, looper);
202 
203         if (handlerHelper == null) {
204             // Would have preferred to not have handlerHelper has nullable and pass this through the
205             // default ctor.  Had trouble doing this because getHander() can't be called until
206             // the type is fully constructed.
207             handlerHelper = new HandlerHelper(getHandler());
208         }
209         mCbSendMessageCalculatorFactory = cbSendMessageCalculatorFactory;
210         mLocationRequester = new LocationRequester(
211                 context,
212                 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE),
213                 handlerHelper, getName());
214 
215         // Adding GSM / CDMA service category mapping.
216         mServiceCategoryCrossRATMap = Stream.of(new Integer[][] {
217                 // Presidential alert
218                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL,
219                         CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT},
220                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE,
221                         CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT},
222 
223                 // Extreme alert
224                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED,
225                         CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
226                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE,
227                         CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
228                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY,
229                         CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
230                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE,
231                         CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
232 
233                 // Severe alert
234                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED,
235                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
236                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE,
237                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
238                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY,
239                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
240                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE,
241                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
242                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED,
243                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
244                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE,
245                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
246                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY,
247                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
248                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE,
249                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
250                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED,
251                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
252                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE,
253                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
254                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY,
255                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
256                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE,
257                         CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
258 
259                 // Amber alert
260                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY,
261                         CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY},
262                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE,
263                         CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY},
264 
265                 // Monthly test alert
266                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST,
267                         CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE},
268                 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE,
269                         CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE},
270         }).collect(Collectors.toMap(data -> data[0], data -> data[1]));
271 
272         IntentFilter intentFilter = new IntentFilter();
273         intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
274         if (IS_DEBUGGABLE) {
275             intentFilter.addAction(ACTION_DUPLICATE_DETECTION);
276         }
277 
278         mContext.registerReceiver(mReceiver, intentFilter);
279     }
280 
cleanup()281     public void cleanup() {
282         if (DBG) log("CellBroadcastHandler cleanup");
283         mContext.unregisterReceiver(mReceiver);
284     }
285 
286     /**
287      * Create a new CellBroadcastHandler.
288      * @param context the context to use for dispatching Intents
289      * @return the new handler
290      */
makeCellBroadcastHandler(Context context)291     public static CellBroadcastHandler makeCellBroadcastHandler(Context context) {
292         CellBroadcastHandler handler = new CellBroadcastHandler(context);
293         handler.start();
294         return handler;
295     }
296 
297     /**
298      * Handle Cell Broadcast messages from {@code CdmaInboundSmsHandler}.
299      * 3GPP-format Cell Broadcast messages sent from radio are handled in the subclass.
300      *
301      * @param message the message to process
302      * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
303      */
304     @Override
handleSmsMessage(Message message)305     protected boolean handleSmsMessage(Message message) {
306         if (message.obj instanceof SmsCbMessage) {
307             if (!isDuplicate((SmsCbMessage) message.obj)) {
308                 handleBroadcastSms((SmsCbMessage) message.obj);
309                 return true;
310             } else {
311                 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED,
312                         CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__CDMA,
313                         CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__DUPLICATE_MESSAGE);
314             }
315             return false;
316         } else {
317             final String errorMessage =
318                     "handleSmsMessage got object of type: " + message.obj.getClass().getName();
319             loge(errorMessage);
320             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
321                     CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_CDMA_MESSAGE_TYPE_FROM_FWK,
322                     errorMessage);
323             return false;
324         }
325     }
326 
327     /**
328      * Get the maximum time for waiting location.
329      *
330      * @param message Cell broadcast message
331      * @return The maximum waiting time in second
332      */
getMaxLocationWaitingTime(SmsCbMessage message)333     protected int getMaxLocationWaitingTime(SmsCbMessage message) {
334         int maximumTime = message.getMaximumWaitingDuration();
335         if (maximumTime == SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET) {
336             Resources res = getResources(message.getSubscriptionId());
337             maximumTime = res.getInteger(R.integer.max_location_waiting_time);
338         }
339         return maximumTime;
340     }
341 
342     /**
343      * Dispatch a Cell Broadcast message to listeners.
344      * @param message the Cell Broadcast to broadcast
345      */
handleBroadcastSms(SmsCbMessage message)346     protected void handleBroadcastSms(SmsCbMessage message) {
347         int slotIndex = message.getSlotIndex();
348 
349         // TODO: Database inserting can be time consuming, therefore this should be changed to
350         // asynchronous.
351         ContentValues cv = message.getContentValues();
352         Uri uri = mContext.getContentResolver().insert(CellBroadcasts.CONTENT_URI, cv);
353 
354         if (message.needGeoFencingCheck()) {
355             int maximumWaitingTime = getMaxLocationWaitingTime(message);
356             if (DBG) {
357                 log("Requesting location for geo-fencing. serialNumber = "
358                         + message.getSerialNumber() + ", maximumWaitingTime = "
359                         + maximumWaitingTime);
360             }
361 
362             CbSendMessageCalculator calculator =
363                     mCbSendMessageCalculatorFactory.createNew(mContext, message.getGeometries());
364             requestLocationUpdate(new LocationUpdateCallback() {
365                 @Override
366                 public void onLocationUpdate(@NonNull LatLng location,
367                         float accuracy) {
368                     if (VDBG) {
369                         logd("onLocationUpdate: location=" + location
370                                 + ", acc=" + accuracy + ". "  + getMessageString(message));
371                     }
372                     performGeoFencing(message, uri, calculator, location, slotIndex,
373                             accuracy);
374                     if (!isMessageInAmbiguousState(calculator)) {
375                         cancelLocationRequest();
376                     }
377                 }
378 
379                 @Override
380                 public void onLocationUnavailable() {
381                     CellBroadcastHandler.this.onLocationUnavailable(
382                             calculator, message, uri, slotIndex);
383                 }
384             }, maximumWaitingTime);
385         } else {
386             if (DBG) {
387                 log("Broadcast the message directly because no geo-fencing required, "
388                         + " needGeoFencing = " + message.needGeoFencingCheck() + ". "
389                         + getMessageString(message));
390             }
391             broadcastMessage(message, uri, slotIndex);
392         }
393     }
394 
395     /**
396      * Returns true if the message calculator is in a non-ambiguous state.
397      *
398      * </b>Note:</b> NO_COORDINATES is considered ambiguous because we received no information
399      * in this case.
400      * @param calculator the message calculator
401      * @return whether or not the message is handled
402      */
isMessageInAmbiguousState(CbSendMessageCalculator calculator)403     protected boolean isMessageInAmbiguousState(CbSendMessageCalculator calculator) {
404         return calculator.getAction() == SEND_MESSAGE_ACTION_AMBIGUOUS
405                 || calculator.getAction() == SEND_MESSAGE_ACTION_NO_COORDINATES;
406     }
407 
408     /**
409      * Cancels the location request
410      */
cancelLocationRequest()411     protected void cancelLocationRequest() {
412         this.mLocationRequester.cancel();
413     }
414 
415     /**
416      * When location requester cannot send anymore updates, we look at the calculated action and
417      * determine whether or not we should send it.
418      *
419      * see: {@code CellBroadcastHandler.LocationUpdateCallback#onLocationUnavailable} for more info.
420      *
421      * @param calculator the send message calculator
422      * @param message the cell broadcast message received
423      * @param uri the message's uri
424      * @param slotIndex the slot
425      */
onLocationUnavailable(CbSendMessageCalculator calculator, SmsCbMessage message, Uri uri, int slotIndex)426     protected void onLocationUnavailable(CbSendMessageCalculator calculator, SmsCbMessage message,
427             Uri uri, int slotIndex) {
428         @CbSendMessageCalculator.SendMessageAction int action = calculator.getAction();
429         if (DBG) {
430             logd("onLocationUnavailable: action="
431                     + CbSendMessageCalculator.getActionString(action) + ". "
432                     + getMessageString(message));
433         }
434 
435         if (isMessageInAmbiguousState(calculator)) {
436             /* Case 1. If we reached the end of the location time out and we are still in an
437                        ambiguous state or no coordinates state, we send the message.
438                Case 2. If we don't have permissions, then no location was received and the
439                        calculator's action is NO_COORDINATES, which means we also send. */
440             broadcastGeofenceMessage(message, uri, slotIndex, calculator);
441         } else if (action == SEND_MESSAGE_ACTION_DONT_SEND) {
442             geofenceMessageNotRequired(message);
443         }
444     }
445 
446     /**
447      * Check the location based on geographical scope defined in 3GPP TS 23.041 section 9.4.1.2.1.
448      *
449      * The Geographical Scope (GS) indicates the geographical area over which the Message Code
450      * is unique, and the display mode. The CBS message is not necessarily broadcast by all cells
451      * within the geographical area. When two CBS messages are received with identical Serial
452      * Numbers/Message Identifiers in two different cells, the Geographical Scope may be used to
453      * determine if the CBS messages are indeed identical.
454      *
455      * @param message The current message
456      * @param messageToCheck The older message in the database to be checked
457      * @return {@code true} if within the same area, otherwise {@code false}, which should be
458      * be considered as a new message.
459      */
isSameLocation(SmsCbMessage message, SmsCbMessage messageToCheck)460     private boolean isSameLocation(SmsCbMessage message,
461             SmsCbMessage messageToCheck) {
462         if (message.getGeographicalScope() != messageToCheck.getGeographicalScope()) {
463             return false;
464         }
465 
466         // only cell wide (which means that if a message is displayed it is desirable that the
467         // message is removed from the screen when the UE selects the next cell and if any CBS
468         // message is received in the next cell it is to be regarded as "new").
469         if (message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE
470                 || message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE) {
471             return message.getLocation().isInLocationArea(messageToCheck.getLocation());
472         }
473 
474         // Service Area wide (which means that a CBS message with the same Message Code and Update
475         // Number may or may not be "new" in the next cell according to whether the next cell is
476         // in the same Service Area as the current cell)
477         if (message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE) {
478             if (!message.getLocation().getPlmn().equals(messageToCheck.getLocation().getPlmn())) {
479                 return false;
480             }
481 
482             return message.getLocation().getLac() != -1
483                     && message.getLocation().getLac() == messageToCheck.getLocation().getLac();
484         }
485 
486         // PLMN wide (which means that the Message Code and/or Update Number must change in the
487         // next cell, of the PLMN, for the CBS message to be "new". The CBS message is only relevant
488         // to the PLMN in which it is broadcast, so any change of PLMN (including a change to
489         // another PLMN which is an ePLMN) means the CBS message is "new")
490         if (message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE) {
491             return !TextUtils.isEmpty(message.getLocation().getPlmn())
492                     && message.getLocation().getPlmn().equals(
493                     messageToCheck.getLocation().getPlmn());
494         }
495 
496         return false;
497     }
498 
499     /**
500      * Check if the message is a duplicate
501      *
502      * @param message Cell broadcast message
503      * @return {@code true} if this message is a duplicate
504      */
505     @VisibleForTesting
isDuplicate(SmsCbMessage message)506     public boolean isDuplicate(SmsCbMessage message) {
507         if (!mEnableDuplicateDetection) {
508             log("Duplicate detection was disabled for debugging purposes.");
509             return false;
510         }
511 
512         // Find the cell broadcast message identify by the message identifier and serial number
513         // and is not broadcasted.
514         String where = CellBroadcasts.RECEIVED_TIME + ">?";
515 
516         Resources res = getResources(message.getSubscriptionId());
517 
518         // Only consider cell broadcast messages received within certain period.
519         // By default it's 24 hours.
520         long expirationDuration = res.getInteger(R.integer.message_expiration_time);
521         long dupCheckTime = System.currentTimeMillis() - expirationDuration;
522 
523         // Some carriers require reset duplication detection after airplane mode or reboot.
524         if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
525             dupCheckTime = Long.max(dupCheckTime, mLastAirplaneModeTime);
526             dupCheckTime = Long.max(dupCheckTime,
527                     System.currentTimeMillis() - SystemClock.elapsedRealtime());
528         }
529 
530         List<SmsCbMessage> cbMessages = new ArrayList<>();
531 
532         try (Cursor cursor = mContext.getContentResolver().query(CellBroadcasts.CONTENT_URI,
533                 CellBroadcastProvider.QUERY_COLUMNS,
534                 where,
535                 new String[] {Long.toString(dupCheckTime)},
536                 null)) {
537             if (cursor != null) {
538                 while (cursor.moveToNext()) {
539                     cbMessages.add(SmsCbMessage.createFromCursor(cursor));
540                 }
541             }
542         }
543 
544         boolean compareMessageBody = res.getBoolean(R.bool.duplicate_compare_body);
545         boolean compareServiceCategory = res.getBoolean(R.bool.duplicate_compare_service_category);
546         boolean crossSimDuplicateDetection = res.getBoolean(R.bool.cross_sim_duplicate_detection);
547 
548         log("Found " + cbMessages.size() + " messages since "
549                 + DateFormat.getDateTimeInstance().format(dupCheckTime));
550         log("compareMessageBody=" + compareMessageBody + ", compareServiceCategory="
551                 + compareServiceCategory + ", crossSimDuplicateDetection="
552                 + crossSimDuplicateDetection);
553         for (SmsCbMessage messageToCheck : cbMessages) {
554             // If messages are from different slots, then we only compare the message body.
555             if (VDBG) log("Checking the message " + messageToCheck);
556             if (crossSimDuplicateDetection
557                     && message.getSlotIndex() != messageToCheck.getSlotIndex()) {
558                 if (TextUtils.equals(message.getMessageBody(), messageToCheck.getMessageBody())) {
559                     log("Duplicate message detected from different slot. " + message);
560                     return true;
561                 }
562                 if (VDBG) log("Not from the same slot.");
563             } else {
564                 // Check serial number if message is from the same carrier.
565                 if (message.getSerialNumber() != messageToCheck.getSerialNumber()) {
566                     if (VDBG) log("Serial number does not match.");
567                     // Not a dup. Check next one.
568                     continue;
569                 }
570 
571                 // ETWS primary / secondary should be treated differently.
572                 if (message.isEtwsMessage() && messageToCheck.isEtwsMessage()
573                         && message.getEtwsWarningInfo().isPrimary()
574                         != messageToCheck.getEtwsWarningInfo().isPrimary()) {
575                     if (VDBG) log("ETWS primary/secondary does not match.");
576                     // Not a dup. Check next one.
577                     continue;
578                 }
579 
580                 // Check if the message category is different.
581                 if (compareServiceCategory
582                         && message.getServiceCategory() != messageToCheck.getServiceCategory()
583                         && !Objects.equals(mServiceCategoryCrossRATMap.get(
584                                 message.getServiceCategory()), messageToCheck.getServiceCategory())
585                         && !Objects.equals(mServiceCategoryCrossRATMap.get(
586                                 messageToCheck.getServiceCategory()),
587                         message.getServiceCategory())) {
588                     if (VDBG) log("Category does not match.");
589                     // Not a dup. Check next one.
590                     continue;
591                 }
592 
593                 // Check if the message location is different. Note this is only applicable to
594                 // 3GPP format cell broadcast messages.
595                 if (message.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP
596                         && messageToCheck.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP
597                         && !isSameLocation(message, messageToCheck)) {
598                     if (VDBG) log("Location does not match.");
599                     // Not a dup. Check next one.
600                     continue;
601                 }
602 
603                 // Compare message body if needed.
604                 if (!compareMessageBody || TextUtils.equals(
605                         message.getMessageBody(), messageToCheck.getMessageBody())) {
606                     log("Duplicate message detected. " + message);
607                     return true;
608                 } else {
609                     if (VDBG) log("Body does not match.");
610                 }
611             }
612         }
613 
614         log("Not a duplicate message. " + message);
615         return false;
616     }
617 
618     /**
619      * Perform a geo-fencing check for {@code message}. Broadcast the {@code message} if the
620      * {@code location} is inside the {@code broadcastArea}.
621      * @param message the message need to geo-fencing check
622      * @param uri the message's uri
623      * @param calculator the message calculator
624      * @param location current location
625      * @param slotIndex the index of the slot
626      * @param accuracy the accuracy of the coordinate given in meters
627      */
performGeoFencing(SmsCbMessage message, Uri uri, CbSendMessageCalculator calculator, LatLng location, int slotIndex, float accuracy)628     protected void performGeoFencing(SmsCbMessage message, Uri uri,
629             CbSendMessageCalculator calculator, LatLng location, int slotIndex, float accuracy) {
630 
631         logd(calculator.toString() + ", current action="
632                 + CbSendMessageCalculator.getActionString(calculator.getAction()));
633 
634         if (calculator.getAction() == SEND_MESSAGE_ACTION_SENT) {
635             if (VDBG) {
636                 logd("performGeoFencing:" + getMessageString(message));
637             }
638             return;
639         }
640 
641         if (uri != null) {
642             ContentValues cv = new ContentValues();
643             cv.put(CellBroadcasts.LOCATION_CHECK_TIME, System.currentTimeMillis());
644             mContext.getContentResolver().update(CellBroadcasts.CONTENT_URI, cv,
645                     CellBroadcasts._ID + "=?", new String[] {uri.getLastPathSegment()});
646         }
647 
648 
649         calculator.addCoordinate(location, accuracy);
650 
651         if (VDBG) {
652             logd("Device location new action = "
653                     + CbSendMessageCalculator.getActionString(calculator.getAction())
654                     + ", threshold = " + calculator.getThreshold()
655                     + ", geos=" + CbGeoUtils.encodeGeometriesToString(calculator.getFences())
656                     + ". " + getMessageString(message));
657         }
658 
659         if (calculator.getAction() == SEND_MESSAGE_ACTION_SEND) {
660             broadcastGeofenceMessage(message, uri, slotIndex, calculator);
661             return;
662         }
663     }
664 
geofenceMessageNotRequired(SmsCbMessage message)665     protected void geofenceMessageNotRequired(SmsCbMessage message) {
666         if (message.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP) {
667             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED,
668                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__GSM,
669                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__GEOFENCED_MESSAGE);
670         } else if (message.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP2) {
671             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED,
672                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__CDMA,
673                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__GEOFENCED_MESSAGE);
674         }
675 
676         sendMessage(EVENT_BROADCAST_NOT_REQUIRED);
677     }
678 
679     /**
680      * Requests a stream of updates for {@code maximumWaitTimeSec} seconds.
681      * @param callback the callback used to communicate back to the caller
682      * @param maximumWaitTimeSec the maximum wait time of this request. If location is not updated
683      * within the maximum wait time, {@code callback#onLocationUnavailable()} will be called.
684      */
requestLocationUpdate(LocationUpdateCallback callback, int maximumWaitTimeSec)685     protected void requestLocationUpdate(LocationUpdateCallback callback, int maximumWaitTimeSec) {
686         mLocationRequester.requestLocationUpdate(callback, maximumWaitTimeSec);
687     }
688 
689     /**
690      * Get the subscription ID for a phone ID, or INVALID_SUBSCRIPTION_ID if the phone does not
691      * have an active sub
692      * @param phoneId the phoneId to use
693      * @return the associated sub id
694      */
getSubIdForPhone(Context context, int phoneId)695     protected static int getSubIdForPhone(Context context, int phoneId) {
696         SubscriptionManager subMan =
697                 (SubscriptionManager) context.getSystemService(
698                         Context.TELEPHONY_SUBSCRIPTION_SERVICE);
699         int[] subIds = subMan.getSubscriptionIds(phoneId);
700         if (subIds != null) {
701             return subIds[0];
702         } else {
703             return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
704         }
705     }
706 
707     /**
708      * Put the phone ID and sub ID into an intent as extras.
709      */
putPhoneIdAndSubIdExtra(Context context, Intent intent, int phoneId)710     public static void putPhoneIdAndSubIdExtra(Context context, Intent intent, int phoneId) {
711         int subId = getSubIdForPhone(context, phoneId);
712         if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
713             intent.putExtra("subscription", subId);
714             intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId);
715         }
716         intent.putExtra("phone", phoneId);
717         intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, phoneId);
718     }
719 
720     /**
721      * Call when dealing with messages that are checked against a geofence.
722      *
723      * @param message the message being broadcast
724      * @param messageUri the message uri
725      * @param slotIndex the slot index
726      * @param calculator the messages send message calculator
727      */
broadcastGeofenceMessage(@onNull SmsCbMessage message, @Nullable Uri messageUri, int slotIndex, CbSendMessageCalculator calculator)728     protected void broadcastGeofenceMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri,
729             int slotIndex, CbSendMessageCalculator calculator) {
730         // Check that the message wasn't already SENT
731         if (calculator.getAction() == CbSendMessageCalculator.SEND_MESSAGE_ACTION_SENT) {
732             return;
733         }
734 
735         if (VDBG) {
736             logd("broadcastGeofenceMessage: mark as sent. " + getMessageString(message));
737         }
738         // Mark the message as SENT
739         calculator.markAsSent();
740 
741         // Broadcast the message
742         broadcastMessage(message, messageUri, slotIndex);
743     }
744 
745     /**
746      * Broadcast the {@code message} to the applications.
747      * @param message a message need to broadcast
748      * @param messageUri message's uri
749      */
broadcastMessage(@onNull SmsCbMessage message, @Nullable Uri messageUri, int slotIndex)750     protected void broadcastMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri,
751             int slotIndex) {
752         String msg;
753         Intent intent;
754         if (VDBG) {
755             logd("broadcastMessage: " + getMessageString(message));
756         }
757 
758         if (message.isEmergencyMessage()) {
759             msg = "Dispatching emergency SMS CB, SmsCbMessage is: " + message;
760             log(msg);
761             mLocalLog.log(msg);
762             intent = new Intent(Telephony.Sms.Intents.ACTION_SMS_EMERGENCY_CB_RECEIVED);
763             //Emergency alerts need to be delivered with high priority
764             intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
765 
766             intent.putExtra(EXTRA_MESSAGE, message);
767             putPhoneIdAndSubIdExtra(mContext, intent, slotIndex);
768 
769             if (IS_DEBUGGABLE) {
770                 // Send additional broadcast intent to the specified package. This is only for sl4a
771                 // automation tests.
772                 String[] testPkgs = mContext.getResources().getStringArray(
773                         R.array.test_cell_broadcast_receiver_packages);
774                 if (testPkgs != null) {
775                     Intent additionalIntent = new Intent(intent);
776                     for (String pkg : testPkgs) {
777                         additionalIntent.setPackage(pkg);
778                         mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
779                                 intent, null, (Bundle) null, null, getHandler(),
780                                 Activity.RESULT_OK, null, null);
781 
782                     }
783                 }
784             }
785 
786             List<String> pkgs = new ArrayList<>();
787             pkgs.add(getDefaultCBRPackageName(mContext, intent));
788             pkgs.addAll(Arrays.asList(mContext.getResources().getStringArray(
789                     R.array.additional_cell_broadcast_receiver_packages)));
790             if (pkgs != null) {
791                 mReceiverCount.addAndGet(pkgs.size());
792                 for (String pkg : pkgs) {
793                     // Explicitly send the intent to all the configured cell broadcast receivers.
794                     intent.setPackage(pkg);
795                     mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
796                             intent, null, (Bundle) null, mOrderedBroadcastReceiver, getHandler(),
797                             Activity.RESULT_OK, null, null);
798                 }
799             }
800         } else {
801             msg = "Dispatching SMS CB, SmsCbMessage is: " + message;
802             log(msg);
803             mLocalLog.log(msg);
804             // Send implicit intent since there are various 3rd party carrier apps listen to
805             // this intent.
806 
807             mReceiverCount.incrementAndGet();
808             CellBroadcastIntents.sendSmsCbReceivedBroadcast(
809                     mContext, UserHandle.ALL, message, mOrderedBroadcastReceiver, getHandler(),
810                     Activity.RESULT_OK, slotIndex);
811         }
812 
813         if (messageUri != null) {
814             ContentValues cv = new ContentValues();
815             cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1);
816             mContext.getContentResolver().update(CellBroadcasts.CONTENT_URI, cv,
817                     CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()});
818         }
819     }
820 
821     /**
822      * Checks if the app's path starts with CB_APEX_PATH
823      */
isAppInCBApexOrAlternativeApp(ApplicationInfo appInfo)824     private static boolean isAppInCBApexOrAlternativeApp(ApplicationInfo appInfo) {
825         return appInfo.sourceDir.startsWith(CB_APEX_PATH) ||
826                appInfo.sourceDir.contains(CB_APP_PLATFORM_NAME);
827     }
828 
829     /**
830      * Find the name of the default CBR package. The criteria is that it belongs to CB apex and
831      * handles the given intent.
832      */
getDefaultCBRPackageName(Context context, Intent intent)833     static String getDefaultCBRPackageName(Context context, Intent intent) {
834         PackageManager packageManager = context.getPackageManager();
835         List<ResolveInfo> cbrPackages = packageManager.queryBroadcastReceivers(intent, 0);
836 
837         // remove apps that don't live in the CellBroadcast apex
838         cbrPackages.removeIf(info ->
839                 !isAppInCBApexOrAlternativeApp(info.activityInfo.applicationInfo));
840 
841         if (cbrPackages.isEmpty()) {
842             Log.e(TAG, "getCBRPackageNames: no package found");
843             return null;
844         }
845 
846         if (cbrPackages.size() > 1) {
847             // multiple apps found, log an error but continue
848             Log.e(TAG, "Found > 1 APK in CB apex that can resolve " + intent.getAction() + ": "
849                     + cbrPackages.stream()
850                     .map(info -> info.activityInfo.applicationInfo.packageName)
851                     .collect(Collectors.joining(", ")));
852         }
853 
854         // Assume the first ResolveInfo is the one we're looking for
855         ResolveInfo info = cbrPackages.get(0);
856         return info.activityInfo.applicationInfo.packageName;
857     }
858 
859     /**
860      * Get the device resource based on SIM
861      *
862      * @param subId Subscription index
863      *
864      * @return The resource
865      */
getResources(int subId)866     public @NonNull Resources getResources(int subId) {
867         if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID
868                 || !SubscriptionManager.isValidSubscriptionId(subId)) {
869             return mContext.getResources();
870         }
871 
872         if (mResourcesCache.containsKey(subId)) {
873             return mResourcesCache.get(subId);
874         }
875 
876         Resources res = SubscriptionManager.getResourcesForSubId(mContext, subId);
877         mResourcesCache.put(subId, res);
878 
879         return res;
880     }
881 
882     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)883     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
884         pw.println("CellBroadcastHandler:");
885         mLocalLog.dump(fd, pw, args);
886         pw.flush();
887     }
888 
889     /** The callback interface of a location request. */
890     public interface LocationUpdateCallback {
891         /**
892          * Called when the location update is available.
893          * @param location a location in (latitude, longitude) format.
894          * @param accuracy the accuracy of the location given from location manager.  Given in
895          *                 meters.
896          */
onLocationUpdate(@onNull LatLng location, float accuracy)897         void onLocationUpdate(@NonNull LatLng location, float accuracy);
898 
899         /**
900          * This is called in the following scenarios:
901          *   1. The max time limit of the LocationRequester was reached, and consequently,
902          *      no more location updates will be sent.
903          *   2. The service does not have permission to request a location update.
904          *   3. The LocationRequester was explicitly cancelled.
905          */
onLocationUnavailable()906         void onLocationUnavailable();
907     }
908 
909     private static final class LocationRequester {
910         /**
911          * Fused location provider, which means GPS plus network based providers (cell, wifi, etc..)
912          */
913         //TODO: Should make LocationManager.FUSED_PROVIDER system API in S.
914         private static final String FUSED_PROVIDER = "fused";
915 
916         /**
917          * The interval in which location requests will be sent.
918          * see more: <code>LocationRequest#setInterval</code>
919          */
920         private static final long LOCATION_REQUEST_INTERVAL_MILLIS = 1000;
921 
922         private final LocationManager mLocationManager;
923         private final List<LocationUpdateCallback> mCallbacks;
924         private final HandlerHelper mHandlerHelper;
925         private final Context mContext;
926         private final LocationListener mLocationListener;
927 
928         private boolean mLocationUpdateInProgress;
929         private final Runnable mLocationUnavailable;
930         private final String mDebugTag;
931 
LocationRequester(Context context, LocationManager locationManager, HandlerHelper handlerHelper, String debugTag)932         LocationRequester(Context context, LocationManager locationManager,
933                 HandlerHelper handlerHelper, String debugTag) {
934             mLocationManager = locationManager;
935             mCallbacks = new ArrayList<>();
936             mContext = context;
937             mHandlerHelper = handlerHelper;
938             mLocationUpdateInProgress = false;
939             mLocationUnavailable = this::onLocationUnavailable;
940 
941             // Location request did not cancel itself when using this::onLocationListener
942             mLocationListener = this::onLocationUpdate;
943             mDebugTag = debugTag;
944         }
945 
946         /**
947          * Requests a stream of updates for {@code maximumWaitTimeSec} seconds.
948          * @param callback the callback used to communicate back to the caller
949          * @param maximumWaitTimeSec the maximum wait time of this request. If location is not
950          *                           updated within the maximum wait time,
951          *                           {@code callback#onLocationUnavailable()} will be called.
952          */
requestLocationUpdate(@onNull LocationUpdateCallback callback, int maximumWaitTimeSec)953         void requestLocationUpdate(@NonNull LocationUpdateCallback callback,
954                 int maximumWaitTimeSec) {
955             mHandlerHelper.post(() -> requestLocationUpdateInternal(callback, maximumWaitTimeSec));
956         }
957 
onLocationUpdate(@onNull Location location)958         private void onLocationUpdate(@NonNull Location location) {
959             if (location == null) {
960                 /* onLocationUpdate should neverreceive a null location, but, covering all of our
961                    bases here. */
962                 Log.wtf(mDebugTag, "Location is never supposed to be null");
963                 return;
964             }
965 
966             LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
967             float accuracy = location.getAccuracy();
968             if (DBG) {
969                 Log.d(mDebugTag, "onLocationUpdate: received location update");
970             }
971 
972             for (LocationUpdateCallback callback : mCallbacks) {
973                 callback.onLocationUpdate(latLng, accuracy);
974             }
975         }
976 
onLocationUnavailable()977         private void onLocationUnavailable() {
978             Log.d(mDebugTag, "onLocationUnavailable: called");
979             locationRequesterCycleComplete();
980         }
981 
982         /* This should only be called if all of the messages are handled. */
cancel()983         public void cancel() {
984             if (mLocationUpdateInProgress) {
985                 Log.d(mDebugTag, "cancel: location update in progress");
986                 mHandlerHelper.removeCallbacks(mLocationUnavailable);
987                 locationRequesterCycleComplete();
988             } else {
989                 Log.d(mDebugTag, "cancel: location update NOT in progress");
990             }
991         }
992 
locationRequesterCycleComplete()993         private void locationRequesterCycleComplete() {
994             try {
995                 for (LocationUpdateCallback callback : mCallbacks) {
996                     callback.onLocationUnavailable();
997                 }
998             } finally {
999                 mLocationManager.removeUpdates(mLocationListener);
1000                 // Reset the state of location requester for the next request
1001                 mCallbacks.clear();
1002                 mLocationUpdateInProgress = false;
1003             }
1004         }
1005 
requestLocationUpdateInternal(@onNull LocationUpdateCallback callback, int maximumWaitTimeS)1006         private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback,
1007                 int maximumWaitTimeS) {
1008             if (DBG) Log.d(mDebugTag, "requestLocationUpdate");
1009             if (!hasPermission(ACCESS_FINE_LOCATION) && !hasPermission(ACCESS_COARSE_LOCATION)) {
1010                 if (DBG) {
1011                     Log.e(mDebugTag,
1012                             "Can't request location update because of no location permission");
1013                 }
1014                 callback.onLocationUnavailable();
1015                 return;
1016             }
1017 
1018             if (!mLocationUpdateInProgress) {
1019                 try {
1020                     // If the user does not turn on location, immediately report location
1021                     // unavailable.
1022                     if (!mLocationManager.isLocationEnabled()) {
1023                         Log.d(mDebugTag, "Location is turned off.");
1024                         callback.onLocationUnavailable();
1025                         return;
1026                     }
1027 
1028                     /* We will continue to send updates until the location timeout is reached. The
1029                     location timeout case is handled through onLocationUnavailable. */
1030                     LocationRequest request = LocationRequest.create()
1031                             .setProvider(FUSED_PROVIDER)
1032                             .setQuality(LocationRequest.ACCURACY_FINE)
1033                             .setInterval(LOCATION_REQUEST_INTERVAL_MILLIS);
1034                     if (DBG) {
1035                         Log.d(mDebugTag, "Location request=" + request);
1036                     }
1037                     mLocationManager.requestLocationUpdates(request,
1038                             new HandlerExecutor(mHandlerHelper.getHandler()),
1039                             mLocationListener);
1040 
1041                     // TODO: Remove the following workaround in S. We need to enforce the timeout
1042                     // before location manager adds the support for timeout value which is less
1043                     // than 30 seconds. After that we can rely on location manager's timeout
1044                     // mechanism.
1045                     mHandlerHelper.postDelayed(mLocationUnavailable,
1046                             TimeUnit.SECONDS.toMillis(maximumWaitTimeS));
1047                 } catch (IllegalArgumentException e) {
1048                     Log.e(mDebugTag, "Cannot get current location. e=" + e);
1049                     callback.onLocationUnavailable();
1050                     return;
1051                 }
1052                 mLocationUpdateInProgress = true;
1053             }
1054             mCallbacks.add(callback);
1055         }
1056 
hasPermission(String permission)1057         private boolean hasPermission(String permission) {
1058             // TODO: remove the check. This will always return true because cell broadcast service
1059             // is running under the UID Process.NETWORK_STACK_UID, which is below 10000. It will be
1060             // automatically granted with all runtime permissions.
1061             return mContext.checkPermission(permission, Process.myPid(), Process.myUid())
1062                     == PackageManager.PERMISSION_GRANTED;
1063         }
1064     }
1065 
1066     /**
1067      * Provides message identifiers that are helpful when logging messages.
1068      *
1069      * @param message the message to log
1070      * @return a helpful message
1071      */
getMessageString(SmsCbMessage message)1072     protected static String getMessageString(SmsCbMessage message) {
1073         return "msg=("
1074                 + message.getServiceCategory() + ","
1075                 + message.getSerialNumber() + ")";
1076     }
1077 
1078 
1079     /**
1080      * Wraps the {@code Handler} in order to mock the methods.
1081      */
1082     @VisibleForTesting
1083     public static class HandlerHelper {
1084 
1085         private final Handler mHandler;
1086 
HandlerHelper(@onNull final Handler handler)1087         public HandlerHelper(@NonNull final Handler handler) {
1088             mHandler = handler;
1089         }
1090 
1091         /**
1092          * Posts {@code r} to {@code handler} with a delay of {@code delayMillis}
1093          *
1094          * @param r the runnable callback
1095          * @param delayMillis the number of milliseconds to delay
1096          */
postDelayed(Runnable r, long delayMillis)1097         public void postDelayed(Runnable r, long delayMillis) {
1098             mHandler.postDelayed(r, delayMillis);
1099         }
1100 
1101         /**
1102          * Posts {@code r} to the underlying handler
1103          *
1104          * @param r the runnable callback
1105          */
post(Runnable r)1106         public void post(Runnable r) {
1107             mHandler.post(r);
1108         }
1109 
1110         /**
1111          * Gets the underlying handler
1112          * @return the handler
1113          */
getHandler()1114         public Handler getHandler() {
1115             return mHandler;
1116         }
1117 
1118         /**
1119          * Remove any pending posts of Runnable r that are in the message queue.
1120          */
removeCallbacks(Runnable r)1121         public void removeCallbacks(Runnable r) {
1122             mHandler.removeCallbacks(r);
1123         }
1124     }
1125 }
1126