blob: fb3677ebfe9706dd31e868195193b59af27aa8fd [file] [log] [blame]
Jordan Liuc872fad2019-10-11 11:42:03 -07001/*
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
17package com.android.cellbroadcastservice;
18
Jordan Liuc872fad2019-10-11 11:42:03 -070019import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG;
20
21import android.Manifest;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.app.Activity;
25import android.app.AppOpsManager;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
Philip P. Moltmann376f9fa2019-10-30 15:25:24 -070029import android.content.pm.PackageManager;
Jordan Liuc872fad2019-10-11 11:42:03 -070030import android.location.Location;
31import android.location.LocationListener;
32import android.location.LocationManager;
33import android.net.Uri;
34import android.os.Build;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.Looper;
38import android.os.Message;
Philip P. Moltmann376f9fa2019-10-30 15:25:24 -070039import android.os.Process;
Jordan Liuc872fad2019-10-11 11:42:03 -070040import android.os.UserHandle;
41import android.provider.Settings;
42import android.provider.Telephony;
43import android.provider.Telephony.CellBroadcasts;
44import android.telephony.SmsCbMessage;
45import android.telephony.SubscriptionManager;
46import android.text.format.DateUtils;
47import android.util.LocalLog;
48import android.util.Log;
49
50import com.android.internal.telephony.CbGeoUtils.Geometry;
51import com.android.internal.telephony.CbGeoUtils.LatLng;
52import com.android.internal.telephony.metrics.TelephonyMetrics;
53
54import java.io.FileDescriptor;
55import java.io.PrintWriter;
56import java.util.ArrayList;
57import java.util.Arrays;
58import java.util.List;
59
60/**
61 * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast
62 * completes and our result receiver is called.
63 */
64public class CellBroadcastHandler extends WakeLockStateMachine {
65 private static final String EXTRA_MESSAGE = "message";
66
67 private final LocalLog mLocalLog = new LocalLog(100);
68
Jordan Liuc872fad2019-10-11 11:42:03 -070069 /** Uses to request the location update. */
70 public final LocationRequester mLocationRequester;
71
72 private CellBroadcastHandler(Context context) {
73 this("CellBroadcastHandler", context);
74 }
75
76 protected CellBroadcastHandler(String debugTag, Context context) {
77 super(debugTag, context);
78 mLocationRequester = new LocationRequester(
79 context,
80 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE),
81 getHandler().getLooper());
82 }
83
84 /**
85 * Create a new CellBroadcastHandler.
86 * @param context the context to use for dispatching Intents
87 * @return the new handler
88 */
89 public static CellBroadcastHandler makeCellBroadcastHandler(Context context) {
90 CellBroadcastHandler handler = new CellBroadcastHandler(context);
91 handler.start();
92 return handler;
93 }
94
95 /**
96 * Handle Cell Broadcast messages from {@code CdmaInboundSmsHandler}.
97 * 3GPP-format Cell Broadcast messages sent from radio are handled in the subclass.
98 *
99 * @param message the message to process
100 * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
101 */
102 @Override
103 protected boolean handleSmsMessage(Message message) {
104 if (message.obj instanceof SmsCbMessage) {
105 handleBroadcastSms((SmsCbMessage) message.obj);
106 return true;
107 } else {
108 loge("handleMessage got object of type: " + message.obj.getClass().getName());
109 return false;
110 }
111 }
112
113 /**
114 * Dispatch a Cell Broadcast message to listeners.
115 * @param message the Cell Broadcast to broadcast
116 */
117 protected void handleBroadcastSms(SmsCbMessage message) {
118 int slotIndex = message.getSlotIndex();
119 // Log Cellbroadcast msg received event
120 TelephonyMetrics metrics = TelephonyMetrics.getInstance();
121 metrics.writeNewCBSms(slotIndex, message.getMessageFormat(),
122 message.getMessagePriority(), message.isCmasMessage(), message.isEtwsMessage(),
123 message.getServiceCategory(), message.getSerialNumber(),
124 System.currentTimeMillis());
125
126 // TODO: Database inserting can be time consuming, therefore this should be changed to
127 // asynchronous.
128 ContentValues cv = message.getContentValues();
Chen Xu93fdfeb2019-10-21 22:56:37 -0700129 Uri uri = mContext.getContentResolver().insert(CellBroadcasts.CONTENT_URI, cv);
Jordan Liuc872fad2019-10-11 11:42:03 -0700130
131 if (message.needGeoFencingCheck()) {
132 if (DBG) {
133 log("Request location update for geo-fencing. serialNumber = "
134 + message.getSerialNumber());
135 }
136
137 requestLocationUpdate(location -> {
138 if (location == null) {
139 // Broadcast the message directly if the location is not available.
140 broadcastMessage(message, uri, slotIndex);
141 } else {
142 performGeoFencing(message, uri, message.getGeometries(), location, slotIndex);
143 }
144 }, message.getMaximumWaitingTime());
145 } else {
146 if (DBG) {
147 log("Broadcast the message directly because no geo-fencing required, "
148 + "serialNumber = " + message.getSerialNumber()
149 + " needGeoFencing = " + message.needGeoFencingCheck());
150 }
151 broadcastMessage(message, uri, slotIndex);
152 }
153 }
154
155 /**
156 * Perform a geo-fencing check for {@code message}. Broadcast the {@code message} if the
157 * {@code location} is inside the {@code broadcastArea}.
158 * @param message the message need to geo-fencing check
159 * @param uri the message's uri
160 * @param broadcastArea the broadcast area of the message
161 * @param location current location
162 */
163 protected void performGeoFencing(SmsCbMessage message, Uri uri, List<Geometry> broadcastArea,
164 LatLng location, int slotIndex) {
165
166 if (DBG) {
167 logd("Perform geo-fencing check for message identifier = "
168 + message.getServiceCategory()
169 + " serialNumber = " + message.getSerialNumber());
170 }
171
172 for (Geometry geo : broadcastArea) {
173 if (geo.contains(location)) {
174 broadcastMessage(message, uri, slotIndex);
175 return;
176 }
177 }
178
179 if (DBG) {
180 logd("Device location is outside the broadcast area "
181 + CbGeoUtils.encodeGeometriesToString(broadcastArea));
182 }
183 }
184
185 /**
186 * Request a single location update.
187 * @param callback a callback will be called when the location is available.
188 * @param maximumWaitTimeSec the maximum wait time of this request. If location is not updated
189 * within the maximum wait time, {@code callback#onLocationUpadte(null)} will be called.
190 */
191 protected void requestLocationUpdate(LocationUpdateCallback callback, int maximumWaitTimeSec) {
192 mLocationRequester.requestLocationUpdate(callback, maximumWaitTimeSec);
193 }
194
195 /**
Jordan Liuc872fad2019-10-11 11:42:03 -0700196 * Broadcast the {@code message} to the applications.
197 * @param message a message need to broadcast
198 * @param messageUri message's uri
199 */
200 protected void broadcastMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri,
201 int slotIndex) {
202 String receiverPermission;
203 int appOp;
204 String msg;
205 Intent intent;
206 if (message.isEmergencyMessage()) {
207 msg = "Dispatching emergency SMS CB, SmsCbMessage is: " + message;
208 log(msg);
209 mLocalLog.log(msg);
210 intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION);
211 //Emergency alerts need to be delivered with high priority
212 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
213 receiverPermission = Manifest.permission.RECEIVE_EMERGENCY_BROADCAST;
214 appOp = AppOpsManager.OP_RECEIVE_EMERGECY_SMS;
215
216 intent.putExtra(EXTRA_MESSAGE, message);
217 SubscriptionManager.putPhoneIdAndSubIdExtra(intent, slotIndex);
218
219 if (Build.IS_DEBUGGABLE) {
220 // Send additional broadcast intent to the specified package. This is only for sl4a
221 // automation tests.
222 final String additionalPackage = Settings.Secure.getString(
223 mContext.getContentResolver(), CMAS_ADDITIONAL_BROADCAST_PKG);
224 if (additionalPackage != null) {
225 Intent additionalIntent = new Intent(intent);
226 additionalIntent.setPackage(additionalPackage);
227 mContext.sendOrderedBroadcastAsUser(additionalIntent, UserHandle.ALL,
228 receiverPermission, appOp, null, getHandler(), Activity.RESULT_OK,
229 null, null);
230 }
231 }
232
233 String[] pkgs = mContext.getResources().getStringArray(
234 com.android.internal.R.array.config_defaultCellBroadcastReceiverPkgs);
235 mReceiverCount.addAndGet(pkgs.length);
236 for (String pkg : pkgs) {
237 // Explicitly send the intent to all the configured cell broadcast receivers.
238 intent.setPackage(pkg);
239 mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission,
240 appOp, mReceiver, getHandler(), Activity.RESULT_OK, null, null);
241 }
242 } else {
243 msg = "Dispatching SMS CB, SmsCbMessage is: " + message;
244 log(msg);
245 mLocalLog.log(msg);
246 intent = new Intent(Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION);
247 // Send implicit intent since there are various 3rd party carrier apps listen to
248 // this intent.
249 intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
250 receiverPermission = Manifest.permission.RECEIVE_SMS;
251 appOp = AppOpsManager.OP_RECEIVE_SMS;
252
253 intent.putExtra(EXTRA_MESSAGE, message);
254 SubscriptionManager.putPhoneIdAndSubIdExtra(intent, slotIndex);
255
256 mReceiverCount.incrementAndGet();
257 mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission, appOp,
258 mReceiver, getHandler(), Activity.RESULT_OK, null, null);
259 }
260
261 if (messageUri != null) {
262 ContentValues cv = new ContentValues();
263 cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1);
Chen Xu93fdfeb2019-10-21 22:56:37 -0700264 mContext.getContentResolver().update(CellBroadcasts.CONTENT_URI, cv,
Jordan Liuc872fad2019-10-11 11:42:03 -0700265 CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()});
266 }
267 }
268
269 @Override
270 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
271 pw.println("CellBroadcastHandler:");
272 mLocalLog.dump(fd, pw, args);
273 pw.flush();
274 }
275
276 /** The callback interface of a location request. */
277 public interface LocationUpdateCallback {
278 /**
279 * Call when the location update is available.
280 * @param location a location in (latitude, longitude) format, or {@code null} if the
281 * location service is not available.
282 */
283 void onLocationUpdate(@Nullable LatLng location);
284 }
285
286 private static final class LocationRequester {
287 private static final String TAG = LocationRequester.class.getSimpleName();
288
289 /**
290 * Use as the default maximum wait time if the cell broadcast doesn't specify the value.
291 * Most of the location request should be responded within 20 seconds.
292 */
293 private static final int DEFAULT_MAXIMUM_WAIT_TIME_SEC = 20;
294
295 /**
296 * Trigger this event when the {@link LocationManager} is not responded within the given
297 * time.
298 */
299 private static final int EVENT_LOCATION_REQUEST_TIMEOUT = 1;
300
301 /** Request a single location update. */
302 private static final int EVENT_REQUEST_LOCATION_UPDATE = 2;
303
304 /**
305 * Request location update from network or gps location provider. Network provider will be
306 * used if available, otherwise use the gps provider.
307 */
308 private static final List<String> LOCATION_PROVIDERS = Arrays.asList(
309 LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER);
310
311 private final LocationManager mLocationManager;
312 private final Looper mLooper;
313 private final List<LocationUpdateCallback> mCallbacks;
314 private final Context mContext;
315 private Handler mLocationHandler;
316
317 LocationRequester(Context context, LocationManager locationManager, Looper looper) {
318 mLocationManager = locationManager;
319 mLooper = looper;
320 mCallbacks = new ArrayList<>();
321 mContext = context;
322 mLocationHandler = new LocationHandler(looper);
323 }
324
325 /**
326 * Request a single location update. If the location is not available, a callback with
327 * {@code null} location will be called immediately.
328 *
329 * @param callback a callback to the response when the location is available
330 * @param maximumWaitTimeSec the maximum wait time of this request. If location is not
331 * updated within the maximum wait time, {@code callback#onLocationUpadte(null)} will be
332 * called.
333 */
334 void requestLocationUpdate(@NonNull LocationUpdateCallback callback,
335 int maximumWaitTimeSec) {
336 mLocationHandler.obtainMessage(EVENT_REQUEST_LOCATION_UPDATE, maximumWaitTimeSec,
337 0 /* arg2 */, callback).sendToTarget();
338 }
339
340 private void onLocationUpdate(@Nullable LatLng location) {
341 for (LocationUpdateCallback callback : mCallbacks) {
342 callback.onLocationUpdate(location);
343 }
344 mCallbacks.clear();
345 }
346
347 private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback,
348 int maximumWaitTimeSec) {
349 if (DBG) Log.d(TAG, "requestLocationUpdate");
350 if (!isLocationServiceAvailable()) {
351 if (DBG) {
352 Log.d(TAG, "Can't request location update because of no location permission");
353 }
354 callback.onLocationUpdate(null);
355 return;
356 }
357
358 if (!mLocationHandler.hasMessages(EVENT_LOCATION_REQUEST_TIMEOUT)) {
359 if (maximumWaitTimeSec == SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET) {
360 maximumWaitTimeSec = DEFAULT_MAXIMUM_WAIT_TIME_SEC;
361 }
362 mLocationHandler.sendMessageDelayed(
363 mLocationHandler.obtainMessage(EVENT_LOCATION_REQUEST_TIMEOUT),
364 maximumWaitTimeSec * DateUtils.SECOND_IN_MILLIS);
365 }
366
367 mCallbacks.add(callback);
368
369 for (String provider : LOCATION_PROVIDERS) {
370 if (mLocationManager.isProviderEnabled(provider)) {
371 mLocationManager.requestSingleUpdate(provider, mLocationListener, mLooper);
372 break;
373 }
374 }
375 }
376
377 private boolean isLocationServiceAvailable() {
378 if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
379 && !hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) return false;
380 for (String provider : LOCATION_PROVIDERS) {
381 if (mLocationManager.isProviderEnabled(provider)) return true;
382 }
383 return false;
384 }
385
386 private boolean hasPermission(String permission) {
Philip P. Moltmann376f9fa2019-10-30 15:25:24 -0700387 return mContext.checkPermission(permission, Process.myPid(), Process.myUid())
388 == PackageManager.PERMISSION_GRANTED;
Jordan Liuc872fad2019-10-11 11:42:03 -0700389 }
390
391 private final LocationListener mLocationListener = new LocationListener() {
392 @Override
393 public void onLocationChanged(Location location) {
394 mLocationHandler.removeMessages(EVENT_LOCATION_REQUEST_TIMEOUT);
395 onLocationUpdate(new LatLng(location.getLatitude(), location.getLongitude()));
396 }
397
398 @Override
399 public void onStatusChanged(String provider, int status, Bundle extras) {}
400
401 @Override
402 public void onProviderEnabled(String provider) {}
403
404 @Override
405 public void onProviderDisabled(String provider) {}
406 };
407
408 private final class LocationHandler extends Handler {
409 LocationHandler(Looper looper) {
410 super(looper);
411 }
412
413 @Override
414 public void handleMessage(Message msg) {
415 switch (msg.what) {
416 case EVENT_LOCATION_REQUEST_TIMEOUT:
417 if (DBG) Log.d(TAG, "location request timeout");
418 onLocationUpdate(null);
419 break;
420 case EVENT_REQUEST_LOCATION_UPDATE:
421 requestLocationUpdateInternal((LocationUpdateCallback) msg.obj, msg.arg1);
422 break;
423 default:
424 Log.e(TAG, "Unsupported message type " + msg.what);
425 }
426 }
427 }
428 }
429}