Add threshold support for WEA 3.0 geofencing
Allows a UE to be outside of a given fence by a given threshold and still receive a Cb alert.
Will also send an alert if the UE has an accuracy that overlaps with the the fence.
Bug: 146417784
Test: Cellbroadcast Tests
Change-Id: I2ab3be69a66a4ea58e0dc9d39958516d5ff709b7
diff --git a/res/values-mcc310-mnc260/config.xml b/res/values-mcc310-mnc260/config.xml
index db9c4a0..16c64ee 100644
--- a/res/values-mcc310-mnc260/config.xml
+++ b/res/values-mcc310-mnc260/config.xml
@@ -21,4 +21,11 @@
<!-- Whether to compare message body when performing message duplicate detection -->
<bool name="duplicate_compare_body">true</bool>
+
+ <!-- A toggle that controls whether or not to use the new geo fence calculation. -->
+ <bool name="use_new_geo_fence_calculation">true</bool>
+
+ <!-- The threshold in which a UE can be outside of the geofence and still
+ receive a Cell Broadcast message. The value given is in meters. -->
+ <integer name="geo_fence_threshold">50</integer>
</resources>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 8fe6aad..9c94c9b 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -50,4 +50,11 @@
<!-- The maximum waiting time in seconds for location to perform
device based geo-fencing -->
<integer name="max_location_waiting_time">30</integer>
+
+ <!-- A toggle that controls whether or not to use the new geo fence calculation. -->
+ <bool name="use_new_geo_fence_calculation">false</bool>
+
+ <!-- The threshold in which a UE can be outside of the geofence and still
+ receive a Cell Broadcast message. The value given is in meters. -->
+ <integer name="geo_fence_threshold">100</integer>
</resources>
diff --git a/src/com/android/cellbroadcastservice/CbGeoUtils.java b/src/com/android/cellbroadcastservice/CbGeoUtils.java
index b308909..59ebd16 100644
--- a/src/com/android/cellbroadcastservice/CbGeoUtils.java
+++ b/src/com/android/cellbroadcastservice/CbGeoUtils.java
@@ -24,8 +24,12 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.stream.Collectors;
/**
@@ -149,4 +153,271 @@
String[] latLng = str.split("\\s*,\\s*");
return new LatLng(Double.parseDouble(latLng[0]), Double.parseDouble(latLng[1]));
}
+
+ private static final double SCALE = 1000.0 * 100.0;
+
+
+ /**
+ * Computes the shortest distance of {@code geo} to {@code latLng}. If {@code geo} does not
+ * support this functionality, {@code Optional.empty()} is returned.
+ *
+ * @hide
+ * @param geo shape
+ * @param latLng point to calculate against
+ * @return the distance in meters
+ */
+ @VisibleForTesting
+ public static Optional<Double> distance(Geometry geo,
+ @NonNull LatLng latLng) {
+ if (geo instanceof android.telephony.CbGeoUtils.Polygon) {
+ CbGeoUtils.DistancePolygon distancePolygon =
+ new CbGeoUtils.DistancePolygon((Polygon) geo);
+ return Optional.of(distancePolygon.distance(latLng));
+ } else if (geo instanceof android.telephony.CbGeoUtils.Circle) {
+ CbGeoUtils.DistanceCircle distanceCircle =
+ new CbGeoUtils.DistanceCircle((Circle) geo);
+ return Optional.of(distanceCircle.distance(latLng));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Will be merged with {@code CbGeoUtils.Circle} in future release.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class DistanceCircle {
+ private final Circle mCircle;
+
+ DistanceCircle(Circle circle) {
+ mCircle = circle;
+ }
+
+ /**
+ * Distance in meters. If you are within the bounds of the circle, returns a
+ * negative distance to the edge.
+ * @param latLng the coordinate to calculate distance against
+ * @return the distance given in meters
+ */
+ public double distance(@NonNull final LatLng latLng) {
+ return latLng.distance(mCircle.getCenter()) - mCircle.getRadius();
+ }
+ }
+ /**
+ * Will be merged with {@code CbGeoUtils.Polygon} in future release.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class DistancePolygon {
+
+ @NonNull private final Polygon mPolygon;
+ @NonNull private final LatLng mOrigin;
+
+ public DistancePolygon(@NonNull final Polygon polygon) {
+ mPolygon = polygon;
+
+ // Find the point with smallest longitude as the mOrigin point.
+ int idx = 0;
+ for (int i = 1; i < polygon.getVertices().size(); i++) {
+ if (polygon.getVertices().get(i).lng < polygon.getVertices().get(idx).lng) {
+ idx = i;
+ }
+ }
+ mOrigin = polygon.getVertices().get(idx);
+ }
+
+ /**
+ * Returns the meters difference between {@code latLng} to the closest point in the polygon.
+ *
+ * Note: The distance given becomes less accurate as you move further north and south.
+ *
+ * @param latLng the coordinate to calculate distance against
+ * @return the distance given in meters
+ */
+ public double distance(@NonNull final LatLng latLng) {
+ double minDistance = Double.MAX_VALUE;
+ List<LatLng> vertices = mPolygon.getVertices();
+ int n = mPolygon.getVertices().size();
+ for (int i = 0; i < n; i++) {
+ LatLng a = vertices.get(i);
+ LatLng b = vertices.get((i + 1) % n);
+
+ // The converted points are distances (in meters) to the origin point.
+ // see: #convertToDistanceFromOrigin
+ Point sa = convertToDistanceFromOrigin(a);
+ Point sb = convertToDistanceFromOrigin(b);
+ Point sp = convertToDistanceFromOrigin(latLng);
+
+ CbGeoUtils.LineSegment l = new CbGeoUtils.LineSegment(sa, sb);
+ double d = l.distance(sp);
+ minDistance = Math.min(d, minDistance);
+ }
+ return minDistance;
+ }
+
+ /**
+ * Move the given point {@code latLng} to the coordinate system with {@code mOrigin} as the
+ * origin. {@code mOrigin} is selected from the vertices of a polygon, it has
+ * the smallest longitude value among all of the polygon vertices. The unit distance
+ * between points is meters.
+ *
+ * @param latLng the point need to be converted and scaled.
+ * @Return a {@link Point} object
+ */
+ private Point convertToDistanceFromOrigin(LatLng latLng) {
+ return CbGeoUtils.convertToDistanceFromOrigin(mOrigin, latLng);
+ }
+ }
+
+ /**
+ * We calculate the new point by finding the distances between the latitude and longitude
+ * components independently from {@code latLng} to the {@code origin}.
+ *
+ * This ends up giving us a {@code Point} such that:
+ * {@code x = distance(latLng.lat, origin.lat)}
+ * {@code y = distance(latLng.lng, origin.lng)}
+ *
+ * This allows us to use simple formulas designed for a cartesian coordinate system when
+ * calculating the distance from a point to a line segment.
+ *
+ * @param origin the origin lat lng in which to convert and scale {@code latLng}
+ * @param latLng the lat lng need to be converted and scaled.
+ * @return a {@link Point} object.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static Point convertToDistanceFromOrigin(@NonNull final LatLng origin,
+ @NonNull final LatLng latLng) {
+ double x = new LatLng(latLng.lat, origin.lng).distance(new LatLng(origin.lat, origin.lng));
+ double y = new LatLng(origin.lat, latLng.lng).distance(new LatLng(origin.lat, origin.lng));
+
+ x = latLng.lat > origin.lat ? x : -x;
+ y = latLng.lng > origin.lng ? y : -y;
+ return new Point(x, y);
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static class Point {
+ /**
+ * x-coordinate
+ */
+ public final double x;
+
+ /**
+ * y-coordinate
+ */
+ public final double y;
+
+ /**
+ * ..ctor
+ * @param x
+ * @param y
+ */
+ public Point(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Subtracts the two points
+ * @param p
+ * @return
+ */
+ public Point subtract(Point p) {
+ return new Point(x - p.x, y - p.y);
+ }
+
+ /**
+ * Calculates the distance between the two points
+ * @param pt
+ * @return
+ */
+ public double distance(Point pt) {
+ return Math.sqrt(Math.pow(x - pt.x, 2) + Math.pow(y - pt.y, 2));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Point point = (Point) o;
+ return Double.compare(point.x, x) == 0
+ && Double.compare(point.y, y) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(x, y);
+ }
+
+ @Override
+ public String toString() {
+ return "(" + x + ", " + y + ")";
+ }
+ }
+
+ /**
+ * Represents a line segment. This is used for handling geo-fenced cell broadcasts.
+ * More information regarding cell broadcast geo-fencing logic is
+ * laid out in 3GPP TS 23.041 and ATIS-0700041.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static final class LineSegment {
+ @NonNull final Point a, b;
+
+ public LineSegment(@NonNull final Point a, @NonNull final Point b) {
+ this.a = a;
+ this.b = b;
+ }
+
+ public double getLength() {
+ return this.a.distance(this.b);
+ }
+
+ /**
+ * Calculates the closest distance from {@code pt} to this line segment.
+ *
+ * @param pt the point to calculate against
+ * @return the distance in meters
+ */
+ public double distance(Point pt) {
+ final double lengthSquared = getLength() * getLength();
+ if (lengthSquared == 0.0) {
+ return pt.distance(this.a);
+ }
+
+ Point sub1 = pt.subtract(a);
+ Point sub2 = b.subtract(a);
+ double dot = sub1.x * sub2.x + sub1.y * sub2.y;
+
+ //Magnitude of projection
+ double magnitude = dot / lengthSquared;
+
+ //Keep bounded between 0.0 and 1.0
+ if (magnitude > 1.0) {
+ magnitude = 1.0;
+ } else if (magnitude < 0.0) {
+ magnitude = 0.0;
+ }
+
+ final double projX = calcProjCoordinate(this.a.x, this.b.x, magnitude);
+ final double projY = calcProjCoordinate(this.a.y, this.b.y, magnitude);
+
+ final Point proj = new Point(projX, projY);
+ return proj.distance(pt);
+ }
+
+ private static double calcProjCoordinate(double aVal, double bVal, double m) {
+ return aVal + ((bVal - aVal) * m);
+ }
+ }
}
diff --git a/src/com/android/cellbroadcastservice/CbSendMessageCalculator.java b/src/com/android/cellbroadcastservice/CbSendMessageCalculator.java
new file mode 100644
index 0000000..8c457c9
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/CbSendMessageCalculator.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.telephony.CbGeoUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Calculates whether or not to send the message according to the inputted geofence.
+ *
+ * Designed to be run multiple times with different calls to #addCoordinate
+ *
+ * @hide
+ *
+ */
+public class CbSendMessageCalculator {
+
+ @NonNull
+ private final List<CbGeoUtils.Geometry> mFences;
+
+ private final double mThresholdMeters;
+ private int mAction = SEND_MESSAGE_ACTION_NO_COORDINATES;
+
+ /*
+ When false, we only check to see if a given coordinate falls within a geo or not.
+ Put another way:
+ 1. The threshold is ignored
+ 2. Ambiguous results are never given
+ */
+ private final boolean mDoNewWay;
+
+ public CbSendMessageCalculator(@NonNull final Context context,
+ @NonNull final List<CbGeoUtils.Geometry> fences) {
+ this(context, fences, context.getResources().getInteger(R.integer.geo_fence_threshold));
+ }
+
+ public CbSendMessageCalculator(@NonNull final Context context,
+ @NonNull final List<CbGeoUtils.Geometry> fences, final double thresholdMeters) {
+
+ mFences = fences.stream().filter(Objects::nonNull).collect(Collectors.toList());
+ mThresholdMeters = thresholdMeters;
+ mDoNewWay = context.getResources().getBoolean(R.bool.use_new_geo_fence_calculation);
+ }
+
+ /**
+ * The given threshold the given coordinates can be outside the geo fence and still receive
+ * {@code SEND_MESSAGE_ACTION_SEND}.
+ *
+ * @return the threshold in meters
+ */
+ public double getThreshold() {
+ return mThresholdMeters;
+ }
+
+ /**
+ * Gets the last action calculated
+ *
+ * @return last action
+ */
+ @SendMessageAction
+ public int getAction() {
+ if (mFences.size() == 0) {
+ return SEND_MESSAGE_ACTION_SEND;
+ }
+
+ return mAction;
+ }
+
+ /**
+ * Translates the action to a readable equivalent
+ * @return readable version of action
+ */
+ String getActionString() {
+ if (mAction == SEND_MESSAGE_ACTION_SEND) {
+ return "SEND";
+ } else if (mAction == SEND_MESSAGE_ACTION_AMBIGUOUS) {
+ return "AMBIGUOUS";
+ } else if (mAction == SEND_MESSAGE_ACTION_DONT_SEND) {
+ return "DONT_SEND";
+ } else if (mAction == SEND_MESSAGE_ACTION_NO_COORDINATES) {
+ return "NO_COORDINATES";
+ } else {
+ return "!BAD_VALUE!";
+ }
+ }
+
+ /** No Coordinates */
+ public static final int SEND_MESSAGE_ACTION_NO_COORDINATES = 0;
+
+ /** Send right away */
+ public static final int SEND_MESSAGE_ACTION_SEND = 1;
+
+ /** Stop waiting for results */
+ public static final int SEND_MESSAGE_ACTION_DONT_SEND = 2;
+
+ /** Continue polling */
+ public static final int SEND_MESSAGE_ACTION_AMBIGUOUS = 3;
+
+ /**
+ * Send Message Action annotation
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SEND_MESSAGE_ACTION_NO_COORDINATES, SEND_MESSAGE_ACTION_SEND,
+ SEND_MESSAGE_ACTION_DONT_SEND, SEND_MESSAGE_ACTION_AMBIGUOUS,
+ })
+ public @interface SendMessageAction {}
+
+ /**
+ * Calculate action based off of the send reason.
+ * @return
+ */
+ public void addCoordinate(CbGeoUtils.LatLng coordinate, double accuracyMeters) {
+ if (mFences.size() == 0) {
+ //No fences mean we shouldn't bother
+ return;
+ }
+
+ calculatePersistentAction(coordinate, accuracyMeters);
+ }
+
+ /** Calculates the state of the next action based off of the new coordinate and the current
+ * action state. According to the rules:
+ * 1. SEND always wins
+ * 2. Outside always trumps an overlap with DONT_SEND
+ * 3. Otherwise we keep an overlap with AMBIGUOUS
+ * @param coordinate the geo location
+ * @param accuracyMeters the accuracy from location manager
+ * @return the action
+ */
+ @SendMessageAction
+ private void calculatePersistentAction(CbGeoUtils.LatLng coordinate, double accuracyMeters) {
+ // If we already marked this as a send, we don't need to check anything.
+ if (this.mAction != SEND_MESSAGE_ACTION_SEND) {
+ @SendMessageAction int newAction =
+ calculateActionFromFences(coordinate, accuracyMeters);
+
+ if (newAction == SEND_MESSAGE_ACTION_SEND) {
+ /* If the new action is in SEND, it doesn't matter what the old action is is. */
+ this.mAction = newAction;
+ } else if (mAction != SEND_MESSAGE_ACTION_DONT_SEND) {
+ /* If the old action is in DONT_SEND, then always overwrite it with ambiguous. */
+ this.mAction = newAction;
+ } else {
+ /* No-op because if we are in a don't send state, we don't want to overwrite
+ with an ambiguous state. */
+ }
+ }
+ }
+
+ /**
+ * Calculates the proposed action state from the fences according to the rules:
+ * 1. Any coordinate with a SEND always wins.
+ * 2. If a coordinate \ accuracy overlaps any fence, go with AMBIGUOUS.
+ * 3. Otherwise, the coordinate is very far outside every fence and we move to DONT_SEND.
+ * @param coordinate the geo location
+ * @param accuracyMeters the accuracy from location manager
+ * @return the action
+ */
+ @SendMessageAction
+ private int calculateActionFromFences(CbGeoUtils.LatLng coordinate, double accuracyMeters) {
+
+ // If everything is outside, then we stick with outside
+ int totalAction = SEND_MESSAGE_ACTION_DONT_SEND;
+ for (int i = 0; i < mFences.size(); i++) {
+ CbGeoUtils.Geometry fence = mFences.get(i);
+ @SendMessageAction
+ final int action = calculateSingleFence(coordinate, accuracyMeters, fence);
+
+ if (action == SEND_MESSAGE_ACTION_SEND) {
+ // The send action means we always go for it.
+ return action;
+ } else if (action == SEND_MESSAGE_ACTION_AMBIGUOUS) {
+ // If we are outside a geo, but then find that the accuracies overlap,
+ // we stick to overlap while still seeing if there are any cases where we are
+ // inside
+ totalAction = SEND_MESSAGE_ACTION_AMBIGUOUS;
+ }
+ }
+ return totalAction;
+ }
+
+ @SendMessageAction
+ private int calculateSingleFence(CbGeoUtils.LatLng coordinate, double accuracyMeters,
+ CbGeoUtils.Geometry fence) {
+ if (fence.contains(coordinate)) {
+ return SEND_MESSAGE_ACTION_SEND;
+ }
+
+ if (mDoNewWay) {
+ return calculateSysSingleFence(coordinate, accuracyMeters, fence);
+ } else {
+ return SEND_MESSAGE_ACTION_DONT_SEND;
+ }
+ }
+
+ private int calculateSysSingleFence(CbGeoUtils.LatLng coordinate, double accuracyMeters,
+ CbGeoUtils.Geometry fence) {
+ Optional<Double> maybeDistance =
+ com.android.cellbroadcastservice.CbGeoUtils.distance(fence, coordinate);
+ if (!maybeDistance.isPresent()) {
+ return SEND_MESSAGE_ACTION_DONT_SEND;
+ }
+
+ double distance = maybeDistance.get();
+ if (accuracyMeters <= mThresholdMeters && distance <= mThresholdMeters) {
+ // The accuracy is precise and we are within the threshold of the boundary, send
+ return SEND_MESSAGE_ACTION_SEND;
+ }
+
+ if (distance <= accuracyMeters) {
+ // Ambiguous case
+ return SEND_MESSAGE_ACTION_AMBIGUOUS;
+ } else {
+ return SEND_MESSAGE_ACTION_DONT_SEND;
+ }
+ }
+}
diff --git a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
index d07a9ae..b0a2d72 100644
--- a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
@@ -19,6 +19,9 @@
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_AMBIGUOUS;
+import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_NO_COORDINATES;
+import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND;
import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_CDMA_MESSAGE_TYPE_FROM_FWK;
import android.annotation.NonNull;
@@ -120,6 +123,7 @@
/** Uses to request the location update. */
private final LocationRequester mLocationRequester;
+ private @NonNull final CbSendMessageCalculatorFactory mCbSendMessageCalculatorFactory;
/** Timestamp of last airplane mode on */
protected long mLastAirplaneModeTime = 0;
@@ -160,12 +164,37 @@
};
private CellBroadcastHandler(Context context) {
- this(CellBroadcastHandler.class.getSimpleName(), context, Looper.myLooper());
+ this(CellBroadcastHandler.class.getSimpleName(), context, Looper.myLooper(),
+ new CbSendMessageCalculatorFactory());
+ }
+
+ /**
+ * Allows tests to inject new calculators
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class CbSendMessageCalculatorFactory {
+ public CbSendMessageCalculatorFactory() {
+ }
+
+ /**
+ * Creates new calculator
+ * @param context context
+ * @param fences the geo fences to use in the calculator
+ * @return a new instance of the calculator
+ */
+ public CbSendMessageCalculator createNew(@NonNull final Context context,
+ @NonNull final List<android.telephony.CbGeoUtils.Geometry> fences) {
+ return new CbSendMessageCalculator(context, fences);
+ }
}
@VisibleForTesting
- public CellBroadcastHandler(String debugTag, Context context, Looper looper) {
+ public CellBroadcastHandler(String debugTag, Context context, Looper looper,
+ @NonNull final CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory) {
super(debugTag, context, looper);
+ mCbSendMessageCalculatorFactory = cbSendMessageCalculatorFactory;
mLocationRequester = new LocationRequester(
context,
(LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE),
@@ -318,12 +347,13 @@
+ maximumWaitingTime);
}
- requestLocationUpdate(location -> {
+ requestLocationUpdate((location, accuracy) -> {
if (location == null) {
// Broadcast the message directly if the location is not available.
broadcastMessage(message, uri, slotIndex);
} else {
- performGeoFencing(message, uri, message.getGeometries(), location, slotIndex);
+ performGeoFencing(message, uri, message.getGeometries(), location, slotIndex,
+ accuracy);
}
}, maximumWaitingTime);
} else {
@@ -507,9 +537,11 @@
* @param uri the message's uri
* @param broadcastArea the broadcast area of the message
* @param location current location
+ * @param slotIndex the index of the slot
+ * @param accuracy the accuracy of the coordinate given in meters
*/
protected void performGeoFencing(SmsCbMessage message, Uri uri, List<Geometry> broadcastArea,
- LatLng location, int slotIndex) {
+ LatLng location, int slotIndex, double accuracy) {
if (DBG) {
logd("Perform geo-fencing check for message identifier = "
@@ -524,16 +556,28 @@
CellBroadcasts._ID + "=?", new String[] {uri.getLastPathSegment()});
}
- for (Geometry geo : broadcastArea) {
- if (geo.contains(location)) {
- broadcastMessage(message, uri, slotIndex);
- return;
+ // When fully implemented, #addCoordinate will be called multiple times and not just once.
+ CbSendMessageCalculator calc =
+ mCbSendMessageCalculatorFactory.createNew(mContext, broadcastArea);
+ calc.addCoordinate(location, accuracy);
+
+ if (calc.getAction() == SEND_MESSAGE_ACTION_SEND
+ || calc.getAction() == SEND_MESSAGE_ACTION_AMBIGUOUS
+ || calc.getAction() == SEND_MESSAGE_ACTION_NO_COORDINATES) {
+ broadcastMessage(message, uri, slotIndex);
+ if (DBG) {
+ Log.d(TAG, "performGeoFencing: SENT. action=" + calc.getActionString()
+ + ", loc=" + location.toString() + ", acc=" + accuracy);
+ calc.getAction();
}
+ return;
}
if (DBG) {
logd("Device location is outside the broadcast area "
+ CbGeoUtils.encodeGeometriesToString(broadcastArea));
+ Log.d(TAG, "performGeoFencing: OUTSIDE. action=" + calc.getAction() + ", loc="
+ + location.toString() + ", acc=" + accuracy);
}
if (message.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP) {
CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED,
@@ -735,7 +779,7 @@
* @param location a location in (latitude, longitude) format, or {@code null} if the
* location service is not available.
*/
- void onLocationUpdate(@Nullable LatLng location);
+ void onLocationUpdate(@Nullable LatLng location, double accuracy);
}
private static final class LocationRequester {
@@ -791,15 +835,17 @@
mLocationUpdateInProgress = false;
mLocationHandler.removeCallbacks(mTimeoutCallback);
LatLng latLng = null;
+ float accuracy = 0;
if (location != null) {
Log.d(TAG, "Got location update");
latLng = new LatLng(location.getLatitude(), location.getLongitude());
+ accuracy = location.getAccuracy();
} else {
Log.e(TAG, "Location is not available.");
}
for (LocationUpdateCallback callback : mCallbacks) {
- callback.onLocationUpdate(latLng);
+ callback.onLocationUpdate(latLng, accuracy);
}
mCallbacks.clear();
}
@@ -811,7 +857,7 @@
if (DBG) {
Log.e(TAG, "Can't request location update because of no location permission");
}
- callback.onLocationUpdate(null);
+ callback.onLocationUpdate(null, Float.NaN);
return;
}
@@ -839,7 +885,7 @@
TimeUnit.SECONDS.toMillis(maximumWaitTimeS));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Cannot get current location. e=" + e);
- callback.onLocationUpdate(null);
+ callback.onLocationUpdate(null, 0.0);
return;
}
mLocationUpdateInProgress = true;
diff --git a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
index dec379d..4f680ec 100644
--- a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
@@ -83,8 +83,9 @@
new HashMap<>(4);
@VisibleForTesting
- public GsmCellBroadcastHandler(Context context, Looper looper) {
- super("GsmCellBroadcastHandler", context, looper);
+ public GsmCellBroadcastHandler(Context context, Looper looper,
+ CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory) {
+ super("GsmCellBroadcastHandler", context, looper, cbSendMessageCalculatorFactory);
}
@Override
@@ -121,7 +122,8 @@
* @return the new handler
*/
public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
- GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper());
+ GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper(),
+ new CbSendMessageCalculatorFactory());
handler.start();
return handler;
}
@@ -216,7 +218,7 @@
return false;
}
- requestLocationUpdate(location -> {
+ requestLocationUpdate((location, accuracy) -> {
if (location == null) {
// If the location is not available, broadcast the messages directly.
for (int i = 0; i < cbMessages.size(); i++) {
@@ -230,7 +232,7 @@
broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
} else {
performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
- location, slotIndex);
+ location, slotIndex, accuracy);
}
}
}
diff --git a/tests/src/com/android/cellbroadcastservice/tests/CbGeoUtilsTest.java b/tests/src/com/android/cellbroadcastservice/tests/CbGeoUtilsTest.java
index aca83ef..6e2a97a 100644
--- a/tests/src/com/android/cellbroadcastservice/tests/CbGeoUtilsTest.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/CbGeoUtilsTest.java
@@ -16,6 +16,9 @@
package com.android.cellbroadcastservice.tests;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.NonNull;
import android.telephony.CbGeoUtils.Circle;
import android.telephony.CbGeoUtils.Geometry;
import android.telephony.CbGeoUtils.LatLng;
@@ -24,18 +27,26 @@
import com.android.cellbroadcastservice.CbGeoUtils;
+import junit.framework.AssertionFailedError;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class CbGeoUtilsTest extends CellBroadcastServiceTestBase {
+ private static final double ASSERT_EQUALS_PRECISION = .005;
+
+ LatLng mGooglePlex = new LatLng(37.423640, -122.088310);
+
@Before
public void setUp() throws Exception {
super.setUp();
@@ -63,4 +74,158 @@
// assert equality
assertEquals(geometries, parsedGeometries);
}
+
+ @Test
+ public void testAddEast() {
+ LatLng east100m = addEast(mGooglePlex, 100);
+ double distance = mGooglePlex.distance(east100m);
+ assertThat(mGooglePlex.lat).isEqualTo(east100m.lat);
+ assertEqualsWithinPrecision(100, distance);
+ }
+
+ @Test
+ public void testAddNorth() {
+ LatLng north120m = addSouth(mGooglePlex, -120);
+ double distance = mGooglePlex.distance(north120m);
+ assertThat(mGooglePlex.lng).isEqualTo(north120m.lng);
+ assertEqualsWithinPrecision(120, distance);
+ }
+
+ @Test
+ public void testExistingLatLngConversionToPoint() {
+ {
+ Circle circle = new Circle(new LatLng(37.42331, -122.08636), 500);
+
+ // ~ 622 meters according to mapping utility
+ LatLng ll622 =
+ new LatLng(37.41807, -122.08389);
+ assertThat(circle.contains(ll622)).isFalse();
+ assertEqualsWithinPrecision(622.0, ll622.distance(circle.getCenter()));
+
+ LatLng origin = circle.getCenter();
+ CbGeoUtils.Point ptCenter = convert(origin, circle.getCenter());
+ CbGeoUtils.Point pt622 = convert(origin, ll622);
+ assertEqualsWithinPrecision(622.0, ptCenter.distance(pt622));
+ }
+
+ {
+ LatLng alaska = new LatLng(61.219005, -149.899929);
+ LatLng llSouth100 = addSouth(alaska, 100);
+ assertEqualsWithinPrecision(100,
+ convert(alaska, llSouth100).distance(convert(alaska, alaska)));
+
+ LatLng llEast100 = addSouth(alaska, 100);
+ assertEqualsWithinPrecision(100,
+ convert(alaska, llEast100).distance(convert(alaska, alaska)));
+
+ LatLng llSouthEast100 = addSouth(addEast(alaska, 100), 100);
+ assertEqualsWithinPrecision(141.42,
+ convert(alaska, llSouthEast100).distance(convert(alaska, alaska)));
+ }
+ }
+
+ @Test
+ public void testDistanceFromSegmentToPerpendicularPoint() {
+ CbGeoUtils.LineSegment seg =
+ new CbGeoUtils.LineSegment(
+ newPoint(0.0, 0.0), newPoint(0.0, 100.0));
+ double dX = seg.distance(newPoint(50.0, 50));
+ assertEqualsWithinPrecision(50.0, dX);
+
+ double dY = seg.distance(newPoint(0.0, 200.0));
+ assertEqualsWithinPrecision(100.0, dY);
+ }
+
+ @Test
+ public void testDistanceFromSegmentToAngledPoint() {
+ {
+ CbGeoUtils.LineSegment seg =
+ new CbGeoUtils.LineSegment(newPoint(0.0, 0.0),
+ newPoint(0.0, 100.0));
+ double hypTriangleOf50 = Math.sqrt(Math.pow(50, 2) + Math.pow(50, 2));
+
+ double upperLeftDistanceSegment = seg.distance(newPoint(-50.0, -50.0));
+ assertEqualsWithinPrecision(hypTriangleOf50, upperLeftDistanceSegment);
+
+ double bottomRightDistanceSegment = seg.distance(newPoint(50.0, 150));
+ assertEqualsWithinPrecision(hypTriangleOf50, bottomRightDistanceSegment);
+ }
+
+ {
+ //Test segment starting with latlng
+ LatLng llWestNorth = mGooglePlex;
+ LatLng llWestSouth = addSouth(mGooglePlex, 500);
+
+ LatLng origin = llWestNorth;
+ CbGeoUtils.Point ptWestNorth = convert(origin, llWestNorth);
+ CbGeoUtils.Point ptWestSouth = convert(origin, llWestSouth);
+ double distancePoints = ptWestNorth.distance(ptWestSouth);
+ assertEqualsWithinPrecision(500, distancePoints);
+ }
+ }
+
+ @Test
+ public void testDistanceWithSquareToPoint() {
+
+ LatLng llWestNorth = addSouth(addEast(mGooglePlex, -100), -100);
+ LatLng llWestSouth = addSouth(addEast(mGooglePlex, -100), 100);
+ LatLng llEastSouth = addSouth(addEast(mGooglePlex, 100), 100);
+ LatLng llEastNorth = addSouth(addEast(mGooglePlex, 100), -100);
+ CbGeoUtils.DistancePolygon square = createPolygon(
+ llWestNorth, llWestSouth, llEastSouth, llEastNorth);
+
+ {
+ LatLng llDueEast = addEast(mGooglePlex, 200);
+ double distance = square.distance(llDueEast);
+ assertEqualsWithinPrecision(100, distance);
+ }
+
+ {
+ LatLng llDueEastNorth = addSouth(addEast(mGooglePlex, 200), -350);
+ double llDistance = square.distance(llDueEastNorth);
+ assertEqualsWithinPrecision(269, llDistance);
+ square.distance(llDueEastNorth);
+ }
+ }
+
+ private CbGeoUtils.DistancePolygon createPolygon(LatLng... latLngs) {
+ return new CbGeoUtils.DistancePolygon(
+ new android.telephony.CbGeoUtils.Polygon(
+ Arrays.stream(latLngs).collect(Collectors.toList())));
+ }
+
+ @NonNull
+ LatLng addEast(LatLng latLng, double meters) {
+ double offset = scaleMeters(meters)
+ / Math.cos(Math.toRadians(latLng.lat));
+ return new LatLng(latLng.lat, latLng.lng + offset);
+ }
+
+ @NonNull
+ LatLng addSouth(LatLng latLng, double meters) {
+ return new LatLng(latLng.lat + scaleMeters(meters),
+ latLng.lng);
+ }
+
+ private static final int EARTH_RADIUS_METER = 6371 * 1000;
+ static double scaleMeters(double meters) {
+ return (meters / EARTH_RADIUS_METER) * (180 / Math.PI);
+ }
+
+ private CbGeoUtils.Point newPoint(double x, double y) {
+ return new CbGeoUtils.Point(x, y);
+ }
+
+ public void assertEqualsWithinPrecision(double expected, double actual) {
+ double precision = expected * ASSERT_EQUALS_PRECISION;
+ if (Math.abs(expected - actual) >= precision) {
+ String message = "assertEqualsWithinPrecision FAILED!\n"
+ + "Expected: " + expected + ", but Actual is: " + actual + "\n";
+ throw new AssertionFailedError(message);
+ }
+ }
+
+ private CbGeoUtils.Point convert(LatLng origin, LatLng latLng) {
+ return CbGeoUtils.convertToDistanceFromOrigin(origin, latLng);
+ }
}
diff --git a/tests/src/com/android/cellbroadcastservice/tests/CbSendMessageCalculatorTest.java b/tests/src/com/android/cellbroadcastservice/tests/CbSendMessageCalculatorTest.java
new file mode 100644
index 0000000..ed5afe0
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/tests/CbSendMessageCalculatorTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice.tests;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.annotation.NonNull;
+import android.telephony.CbGeoUtils.Circle;
+import android.telephony.CbGeoUtils.Geometry;
+import android.telephony.CbGeoUtils.LatLng;
+import android.telephony.CbGeoUtils.Polygon;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.cellbroadcastservice.CbSendMessageCalculator;
+import com.android.cellbroadcastservice.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class CbSendMessageCalculatorTest extends CellBroadcastServiceTestBase {
+ LatLng mGooglePlex = new LatLng(
+ 37.423640, -122.088310);
+
+ private Polygon mSquare;
+ private Circle mCircleFarAway;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ doReturn(true).when(mMockedResources)
+ .getBoolean(R.bool.use_new_geo_fence_calculation);
+
+ LatLng llWestNorth = addSouth(addEast(mGooglePlex, -1000), -1000);
+ LatLng llWestSouth = addSouth(addEast(mGooglePlex, -1000), 1000);
+ LatLng llEastSouth = addSouth(addEast(mGooglePlex, 1000), 1000);
+ LatLng llEastNorth = addSouth(addEast(mGooglePlex, 1000), -1000);
+
+ mSquare = createPolygon(llWestNorth, llWestSouth, llEastSouth, llEastNorth);
+ mCircleFarAway = new Circle(addEast(mGooglePlex, 10000), 10);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testSingleSquareOutside() {
+ LatLng coor = addSouth(addEast(mGooglePlex, -2000), -500);
+ double threshold = 100;
+
+ //We need to test with accuracies less than and greater than the threshold
+ testSquareOutside(coor, threshold - 10, threshold);
+ testSquareOutside(coor, threshold + 10, threshold);
+ }
+
+ void testSquareOutside(LatLng coor, double accuracy, double threshold) {
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+ calculator.addCoordinate(coor, accuracy);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+ }
+
+ @Test
+ public void testSingleSquareInside() {
+ LatLng coor = mGooglePlex;
+ double threshold = 100;
+ testSquareInside(coor, threshold - 10, threshold);
+ testSquareInside(coor, threshold + 10, threshold);
+ }
+
+ void testSquareInside(LatLng coor, double accuracy, double threshold) {
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+ calculator.addCoordinate(coor, accuracy);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND, calculator.getAction());
+ }
+
+ @Test
+ public void testSingleSquareInsideAndOutsideThreshold() {
+ LatLng coor = addEast(addSouth(mGooglePlex, 1005), 1005);
+ double threshold = 100;
+ testSquareInsideThreshold(coor, 1, threshold);
+
+ //The accuracy is greater than the threshold and not overlapping, and so this is a no go
+ testSquareAmbiguous(coor, threshold + 50, threshold);
+ }
+
+ private void testSquareAmbiguous(LatLng coor, double accuracy, double threshold) {
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+ calculator.addCoordinate(coor, accuracy);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_AMBIGUOUS,
+ calculator.getAction());
+ }
+
+ void testSquareInsideThreshold(LatLng coor, double accuracy, double threshold) {
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+ calculator.addCoordinate(coor, accuracy);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND,
+ calculator.getAction());
+ }
+
+ @Test
+ public void testNone() {
+ CbSendMessageCalculator calculator = createCalculator(10, null);
+ new CbSendMessageCalculator(mMockedContext, new ArrayList<>(), 10);
+ calculator.addCoordinate(new LatLng(0, 0), 100);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND,
+ calculator.getAction());
+ }
+
+ @Test
+ public void testMultipleAddsWithOnceSendAlwaysSend() {
+ // Once we are set to send, we always go with send.
+ // Set to inside
+ double threshold = 100;
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+
+ // Inside
+ calculator.addCoordinate(mGooglePlex, threshold + 2000);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND, calculator.getAction());
+
+ // Outside
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, -2000), -500), threshold + 10);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND, calculator.getAction());
+
+ // Ambiguous
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, 1040), 1040), threshold + 50);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND, calculator.getAction());
+ }
+
+ @Test
+ public void testMultipleAddsWithDontSendThenSend() {
+ double threshold = 100;
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+
+ // Outside
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, -2000), -500), threshold + 10);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+
+ // Ambiguous
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, 1040), 1040), threshold + 50);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+
+ // Outside again
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, -2000), -500), threshold + 10);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+
+ // Inside
+ calculator.addCoordinate(mGooglePlex, threshold - 50);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND, calculator.getAction());
+ }
+
+ @Test
+ public void testMultipleAddsWithAmbiguousToDontSend() {
+ double threshold = 100;
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+
+ // Ambiguous
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, 1040), 1040), threshold + 50);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_AMBIGUOUS, calculator.getAction());
+
+ // Ambiguous again
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, 1040), 1040), threshold + 55);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_AMBIGUOUS, calculator.getAction());
+
+ // Outside
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, -2000), -500), threshold + 10);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+
+ // Inside
+ calculator.addCoordinate(mGooglePlex, threshold);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND, calculator.getAction());
+ }
+
+ @Test
+ public void testMultipleAddsAndDoNewWayFalseWithAmbiguousToDontSend() {
+ double threshold = 100;
+
+ //Testing with the geo fence calculation false
+ doReturn(false).when(mMockedResources)
+ .getBoolean(R.bool.use_new_geo_fence_calculation);
+ CbSendMessageCalculator calculator = createCalculator(threshold, mSquare);
+
+ // Ambiguous
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, 1040), 1040), threshold + 50);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+
+ // Ambiguous again
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, 1040), 1040), threshold + 55);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+
+ // Outside
+ calculator.addCoordinate(addSouth(addEast(mGooglePlex, -2000), -500), threshold + 10);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND, calculator.getAction());
+
+ // Inside
+ calculator.addCoordinate(mGooglePlex, threshold);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND, calculator.getAction());
+ }
+
+ @Test public void testThreshold() {
+ //Testing with the geo fence calculation false
+ doReturn(1000).when(mMockedResources)
+ .getInteger(R.integer.geo_fence_threshold);
+
+ List<Geometry> l = new ArrayList<>();
+ l.add(mCircleFarAway);
+ CbSendMessageCalculator calculator = new CbSendMessageCalculator(mMockedContext, l);
+ assertEquals(calculator.getThreshold(), 1000.0);
+ }
+
+ @Test
+ public void testManyGeosWithInsideAndOutside() {
+ double threshold = 100;
+ CbSendMessageCalculator calcOutsideInside =
+ createCalculator(threshold, mCircleFarAway, mSquare);
+
+ // Inside
+ calcOutsideInside.addCoordinate(mGooglePlex, threshold);
+ assertEquals(CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND,
+ calcOutsideInside.getAction());
+ }
+
+ private CbSendMessageCalculator createCalculator(double threshold,
+ Geometry geo, Geometry... geos) {
+ List<Geometry> list = new ArrayList<>(Arrays.asList(geos));
+ list.add(geo);
+ return new CbSendMessageCalculator(mMockedContext, list, threshold);
+ }
+
+ @NonNull
+ LatLng addEast(LatLng latLng, double meters) {
+ double offset = scaleMeters(meters)
+ / Math.cos(Math.toRadians(latLng.lat));
+ return new LatLng(latLng.lat, latLng.lng + offset);
+ }
+
+ @NonNull
+ LatLng addSouth(LatLng latLng, double meters) {
+ return new LatLng(latLng.lat + scaleMeters(meters),
+ latLng.lng);
+ }
+
+ private static final int EARTH_RADIUS_METER = 6371 * 1000;
+ static double scaleMeters(double meters) {
+ return (meters / EARTH_RADIUS_METER) * (180 / Math.PI);
+ }
+
+ private Polygon createPolygon(LatLng... latLngs) {
+ return new Polygon(Arrays.stream(latLngs).collect(Collectors.toList()));
+ }
+}
diff --git a/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastHandlerTest.java b/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastHandlerTest.java
index 5d5ca12..e7245ae 100644
--- a/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastHandlerTest.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastHandlerTest.java
@@ -20,6 +20,7 @@
import static org.mockito.Mockito.doReturn;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
@@ -27,6 +28,7 @@
import android.net.Uri;
import android.os.SystemProperties;
import android.provider.Telephony;
+import android.telephony.CbGeoUtils;
import android.telephony.SmsCbCmasInfo;
import android.telephony.SmsCbLocation;
import android.telephony.SmsCbMessage;
@@ -38,6 +40,9 @@
import android.testing.TestableLooper;
import android.text.format.DateUtils;
+import androidx.annotation.NonNull;
+
+import com.android.cellbroadcastservice.CbSendMessageCalculator;
import com.android.cellbroadcastservice.CellBroadcastHandler;
import com.android.cellbroadcastservice.CellBroadcastProvider;
import com.android.cellbroadcastservice.SmsCbConstants;
@@ -51,6 +56,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.util.List;
import java.util.Map;
@RunWith(AndroidTestingRunner.class)
@@ -64,6 +70,8 @@
@Mock
private Map<Integer, Resources> mMockedResourcesCache;
+ private CbSendMessageCalculatorFactoryFacade mSendMessageFactory;
+
private class CellBroadcastContentProvider extends MockContentProvider {
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
@@ -120,9 +128,10 @@
super.setUp();
mTestbleLooper = TestableLooper.get(CellBroadcastHandlerTest.this);
+ mSendMessageFactory = new CbSendMessageCalculatorFactoryFacade();
mCellBroadcastHandler = new CellBroadcastHandler("CellBroadcastHandlerUT",
- mMockedContext, mTestbleLooper.getLooper());
+ mMockedContext, mTestbleLooper.getLooper(), mSendMessageFactory);
((MockContentResolver) mMockedContext.getContentResolver()).addProvider(
Telephony.CellBroadcasts.CONTENT_URI.getAuthority(),
new CellBroadcastContentProvider());
@@ -266,4 +275,33 @@
assertTrue(mCellBroadcastHandler.isDuplicate(msg));
}
}
+
+ /**
+ * Makes injecting a mock factory easy.
+ */
+ static class CbSendMessageCalculatorFactoryFacade extends
+ CellBroadcastHandler.CbSendMessageCalculatorFactory {
+
+ @NonNull
+ private CellBroadcastHandler.CbSendMessageCalculatorFactory mUnderlyingFactory;
+
+ @NonNull CellBroadcastHandler.CbSendMessageCalculatorFactory getUnderlyingFactory() {
+ return mUnderlyingFactory;
+ }
+
+ void setUnderlyingFactory(
+ @NonNull final CellBroadcastHandler.CbSendMessageCalculatorFactory factory) {
+ mUnderlyingFactory = factory;
+ }
+
+ CbSendMessageCalculatorFactoryFacade() {
+ mUnderlyingFactory = new CellBroadcastHandler.CbSendMessageCalculatorFactory();
+ }
+
+ @Override
+ public CbSendMessageCalculator createNew(@NonNull Context context,
+ @NonNull List<CbGeoUtils.Geometry> fences) {
+ return mUnderlyingFactory.createNew(context, fences);
+ }
+ }
}
diff --git a/tests/src/com/android/cellbroadcastservice/tests/GsmCellBroadcastHandlerTest.java b/tests/src/com/android/cellbroadcastservice/tests/GsmCellBroadcastHandlerTest.java
index e94aabb..d11569b 100644
--- a/tests/src/com/android/cellbroadcastservice/tests/GsmCellBroadcastHandlerTest.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/GsmCellBroadcastHandlerTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -49,6 +50,7 @@
import android.testing.TestableLooper;
import android.text.format.DateUtils;
+import com.android.cellbroadcastservice.CbSendMessageCalculator;
import com.android.cellbroadcastservice.CellBroadcastHandler;
import com.android.cellbroadcastservice.CellBroadcastProvider;
import com.android.cellbroadcastservice.GsmCellBroadcastHandler;
@@ -60,7 +62,6 @@
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
-import org.mockito.Mockito;
import java.util.Map;
import java.util.function.Consumer;
@@ -75,6 +76,7 @@
@Mock
private Map<Integer, Resources> mMockedResourcesCache;
+ private CellBroadcastHandlerTest.CbSendMessageCalculatorFactoryFacade mSendMessageFactory;
private class CellBroadcastContentProvider extends MockContentProvider {
@Override
@@ -156,8 +158,9 @@
super.setUp();
mTestableLooper = TestableLooper.get(GsmCellBroadcastHandlerTest.this);
+ mSendMessageFactory = new CellBroadcastHandlerTest.CbSendMessageCalculatorFactoryFacade();
mGsmCellBroadcastHandler = new GsmCellBroadcastHandler(mMockedContext,
- mTestableLooper.getLooper());
+ mTestableLooper.getLooper(), mSendMessageFactory);
mGsmCellBroadcastHandler.start();
((MockContentResolver) mMockedContext.getContentResolver()).addProvider(
@@ -195,7 +198,7 @@
any(LocationRequest.class), any(), any(), consumerCaptor.capture());
Consumer<Location> consumer = consumerCaptor.getValue();
- consumer.accept(Mockito.mock(Location.class));
+ consumer.accept(mock(Location.class));
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockedContext).sendOrderedBroadcast(intentCaptor.capture(), any(),
@@ -245,7 +248,7 @@
any(LocationRequest.class), any(), any(), captor.capture());
Consumer<Location> consumer = captor.getValue();
- consumer.accept(Mockito.mock(Location.class));
+ consumer.accept(mock(Location.class));
verify(mMockedContext, never()).sendOrderedBroadcast(any(), anyString(), anyString(),
any(), any(), anyInt(), any(), any());
@@ -253,6 +256,44 @@
@Test
@SmallTest
+ public void testGeofencingAmgiguous() {
+ CbSendMessageCalculator mockCalculator = mock(CbSendMessageCalculator.class);
+ CellBroadcastHandler.CbSendMessageCalculatorFactory factory = mock(
+ CellBroadcastHandler.CbSendMessageCalculatorFactory.class);
+ mSendMessageFactory.setUnderlyingFactory(factory);
+ doReturn(mockCalculator).when(factory).createNew(any(), any());
+ doReturn(CbSendMessageCalculator.SEND_MESSAGE_ACTION_AMBIGUOUS)
+ .when(mockCalculator)
+ .getAction();
+
+ // This method is copied form #testGeofencingAlertOutOfPolygon that does NOT send a message.
+ // Except, in this case, we are overriding the calculator with DONT_SEND and so our
+ // verification is that a broadcast was sent.
+ final byte[] pdu = hexStringToBytes("01111D7090010254747A0E4ACF416110B538A582DE6650906AA28"
+ + "2AE6979995D9ECF41C576597E2EBBC77950905D96D3D3EE33689A9FD3CB6D1708CA2E87E76550FAE"
+ + "C7ECBCB203ABA0C6A97E7F3F0B9EC02C15CB5769A5D0652A030FB1ECECF5D5076393C2F83C8E9B9B"
+ + "C7C0ECBC9203A3A3D07B5CBF379F85C06E16030580D660BB662B51A0D57CC3500000000000000000"
+ + "0000000000000000000000000000000000000000000000000003021002078B53B6CA4B84B53988A4"
+ + "B86B53958A4C2DB53B54A4C28B53B6CA4B840100CFF");
+ mGsmCellBroadcastHandler.onGsmCellBroadcastSms(0, pdu);
+ mTestableLooper.processAllMessages();
+
+ ArgumentCaptor<Consumer<Location>> captor = ArgumentCaptor.forClass(Consumer.class);
+ verify(mMockedLocationManager).getCurrentLocation(
+ any(LocationRequest.class), any(), any(), captor.capture());
+
+ Consumer<Location> consumer = captor.getValue();
+ consumer.accept(mock(Location.class));
+
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockedContext).sendOrderedBroadcast(intentCaptor.capture(), any(),
+ (Bundle) any(), any(), any(), anyInt(), any(), any());
+ Intent intent = intentCaptor.getValue();
+ assertEquals(Telephony.Sms.Intents.ACTION_SMS_EMERGENCY_CB_RECEIVED, intent.getAction());
+ }
+
+ @Test
+ @SmallTest
public void testSmsCbLocation() {
final byte[] pdu = hexStringToBytes("01111B40110101C366701A09368545692408000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000"
@@ -263,7 +304,7 @@
final int fakeCid = 5678;
doReturn(fakePlmn).when(mMockedTelephonyManager).getNetworkOperator();
- ServiceState ss = Mockito.mock(ServiceState.class);
+ ServiceState ss = mock(ServiceState.class);
doReturn(ss).when(mMockedTelephonyManager).getServiceState();
NetworkRegistrationInfo nri = new NetworkRegistrationInfo.Builder()
.setDomain(NetworkRegistrationInfo.DOMAIN_CS)
@@ -289,4 +330,46 @@
assertEquals(fakeTac, location.getLac());
assertEquals(fakeCid, location.getCid());
}
+
+ @Test
+ @SmallTest
+ public void testGeofencingDontSend() {
+ CbSendMessageCalculator mockCalculator = mock(CbSendMessageCalculator.class);
+ CellBroadcastHandler.CbSendMessageCalculatorFactory factory = mock(
+ CellBroadcastHandler.CbSendMessageCalculatorFactory.class);
+ mSendMessageFactory.setUnderlyingFactory(factory);
+ doReturn(mockCalculator).when(factory).createNew(any(), any());
+ doReturn(CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND)
+ .when(mockCalculator)
+ .getAction();
+
+ // This method is copied form #testSmsCbLocation that sends out a message. Except, in
+ // this case, we are overriding the calculator with DONT_SEND and so our verification is
+ // is that no broadcast was sent.
+ final byte[] pdu = hexStringToBytes("01111B40110101C366701A09368545692408000000000000000"
+ + "00000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ + "000000000000000000000000000000000000000000000000B");
+
+ final String fakePlmn = "310999";
+ final int fakeTac = 1234;
+ final int fakeCid = 5678;
+
+ doReturn(fakePlmn).when(mMockedTelephonyManager).getNetworkOperator();
+ ServiceState ss = mock(ServiceState.class);
+ doReturn(ss).when(mMockedTelephonyManager).getServiceState();
+ NetworkRegistrationInfo nri = new NetworkRegistrationInfo.Builder()
+ .setDomain(NetworkRegistrationInfo.DOMAIN_CS)
+ .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+ .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
+ .setRegistrationState(NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
+ .setCellIdentity(new CellIdentityLte(0, 0, fakeCid, 0, fakeTac))
+ .build();
+ doReturn(nri).when(ss).getNetworkRegistrationInfo(anyInt(), anyInt());
+
+ mGsmCellBroadcastHandler.onGsmCellBroadcastSms(0, pdu);
+ mTestableLooper.processAllMessages();
+
+ verify(mMockedContext, never()).sendOrderedBroadcast(any(), anyString(), anyString(),
+ any(), any(), anyInt(), any(), any());
+ }
}