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());
+    }
 }