1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cellbroadcastservice; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.content.Context; 22 import android.telephony.CbGeoUtils; 23 24 import java.lang.annotation.Retention; 25 import java.lang.annotation.RetentionPolicy; 26 import java.util.List; 27 import java.util.Objects; 28 import java.util.Optional; 29 import java.util.stream.Collectors; 30 31 /** 32 * Calculates whether or not to send the message according to the inputted geofence. 33 * 34 * Designed to be run multiple times with different calls to #addCoordinate 35 * 36 * @hide 37 * 38 */ 39 public class CbSendMessageCalculator { 40 41 @NonNull 42 private final List<CbGeoUtils.Geometry> mFences; 43 44 private final double mThresholdMeters; 45 private int mAction = SEND_MESSAGE_ACTION_NO_COORDINATES; 46 47 /* 48 When false, we only check to see if a given coordinate falls within a geo or not. 49 Put another way: 50 1. The threshold is ignored 51 2. Ambiguous results are never given 52 */ 53 private final boolean mDoNewWay; 54 CbSendMessageCalculator(@onNull final Context context, @NonNull final List<CbGeoUtils.Geometry> fences)55 public CbSendMessageCalculator(@NonNull final Context context, 56 @NonNull final List<CbGeoUtils.Geometry> fences) { 57 this(context, fences, context.getResources().getInteger(R.integer.geo_fence_threshold)); 58 } 59 CbSendMessageCalculator(@onNull final Context context, @NonNull final List<CbGeoUtils.Geometry> fences, final double thresholdMeters)60 public CbSendMessageCalculator(@NonNull final Context context, 61 @NonNull final List<CbGeoUtils.Geometry> fences, final double thresholdMeters) { 62 63 mFences = fences.stream().filter(Objects::nonNull).collect(Collectors.toList()); 64 mThresholdMeters = thresholdMeters; 65 mDoNewWay = context.getResources().getBoolean(R.bool.use_new_geo_fence_calculation); 66 } 67 68 /** 69 * The given threshold the given coordinates can be outside the geo fence and still receive 70 * {@code SEND_MESSAGE_ACTION_SEND}. 71 * 72 * @return the threshold in meters 73 */ getThreshold()74 public double getThreshold() { 75 return mThresholdMeters; 76 } 77 78 /** 79 * Gets the last action calculated 80 * 81 * @return last action 82 */ 83 @SendMessageAction getAction()84 public int getAction() { 85 if (mFences.size() == 0) { 86 return SEND_MESSAGE_ACTION_SEND; 87 } 88 89 return mAction; 90 } 91 92 /** 93 * Marks the message as being sent. The state will not be changed after this is set. 94 */ markAsSent()95 public void markAsSent() { 96 this.mAction = SEND_MESSAGE_ACTION_SENT; 97 } 98 99 /** 100 * Translates the action to a readable equivalent 101 * @return readable version of action 102 */ getActionString(int action)103 public static String getActionString(int action) { 104 if (action == SEND_MESSAGE_ACTION_SEND) { 105 return "SEND"; 106 } else if (action == SEND_MESSAGE_ACTION_AMBIGUOUS) { 107 return "AMBIGUOUS"; 108 } else if (action == SEND_MESSAGE_ACTION_DONT_SEND) { 109 return "DONT_SEND"; 110 } else if (action == SEND_MESSAGE_ACTION_NO_COORDINATES) { 111 return "NO_COORDINATES"; 112 } else if (action == SEND_MESSAGE_ACTION_SENT) { 113 return "SENT"; 114 } else { 115 return "!BAD_VALUE!"; 116 } 117 } 118 119 /** No Coordinates */ 120 public static final int SEND_MESSAGE_ACTION_NO_COORDINATES = 0; 121 122 /** Send right away */ 123 public static final int SEND_MESSAGE_ACTION_SEND = 1; 124 125 /** Stop waiting for results */ 126 public static final int SEND_MESSAGE_ACTION_DONT_SEND = 2; 127 128 /** Continue polling */ 129 public static final int SEND_MESSAGE_ACTION_AMBIGUOUS = 3; 130 131 /** A user set flag that indicates this message was sent */ 132 public static final int SEND_MESSAGE_ACTION_SENT = 4; 133 134 /** 135 * Get the Geo Fences 136 * 137 * @return a list of shapes 138 */ getFences()139 public @NonNull List<CbGeoUtils.Geometry> getFences() { 140 return this.mFences; 141 } 142 143 /** 144 * Send Message Action annotation 145 */ 146 @Retention(RetentionPolicy.SOURCE) 147 @IntDef({SEND_MESSAGE_ACTION_NO_COORDINATES, SEND_MESSAGE_ACTION_SEND, 148 SEND_MESSAGE_ACTION_DONT_SEND, SEND_MESSAGE_ACTION_AMBIGUOUS, 149 SEND_MESSAGE_ACTION_SENT, 150 }) 151 public @interface SendMessageAction {} 152 153 /** 154 * Calculate action based off of the send reason. 155 * @return 156 */ addCoordinate(CbGeoUtils.LatLng coordinate, float accuracyMeters)157 public void addCoordinate(CbGeoUtils.LatLng coordinate, float accuracyMeters) { 158 if (mFences.size() == 0) { 159 //No fences mean we shouldn't bother 160 return; 161 } 162 163 calculatePersistentAction(coordinate, accuracyMeters); 164 } 165 166 /** Calculates the state of the next action based off of the new coordinate and the current 167 * action state. According to the rules: 168 * 1. SEND always wins 169 * 2. Outside always trumps an overlap with DONT_SEND 170 * 3. Otherwise we keep an overlap with AMBIGUOUS 171 * @param coordinate the geo location 172 * @param accuracyMeters the accuracy from location manager 173 * @return the action 174 */ 175 @SendMessageAction calculatePersistentAction(CbGeoUtils.LatLng coordinate, float accuracyMeters)176 private void calculatePersistentAction(CbGeoUtils.LatLng coordinate, float accuracyMeters) { 177 // If we already marked this as a send, we don't need to check anything. 178 if (this.mAction != SEND_MESSAGE_ACTION_SEND) { 179 @SendMessageAction int newAction = 180 calculateActionFromFences(coordinate, accuracyMeters); 181 182 if (newAction == SEND_MESSAGE_ACTION_SEND) { 183 /* If the new action is in SEND, it doesn't matter what the old action is is. */ 184 this.mAction = newAction; 185 } else if (mAction != SEND_MESSAGE_ACTION_DONT_SEND) { 186 /* If the old action is in DONT_SEND, then always overwrite it with ambiguous. */ 187 this.mAction = newAction; 188 } else { 189 /* No-op because if we are in a don't send state, we don't want to overwrite 190 with an ambiguous state. */ 191 } 192 } 193 } 194 195 /** 196 * Calculates the proposed action state from the fences according to the rules: 197 * 1. Any coordinate with a SEND always wins. 198 * 2. If a coordinate \ accuracy overlaps any fence, go with AMBIGUOUS. 199 * 3. Otherwise, the coordinate is very far outside every fence and we move to DONT_SEND. 200 * @param coordinate the geo location 201 * @param accuracyMeters the accuracy from location manager 202 * @return the action 203 */ 204 @SendMessageAction calculateActionFromFences(CbGeoUtils.LatLng coordinate, float accuracyMeters)205 private int calculateActionFromFences(CbGeoUtils.LatLng coordinate, float accuracyMeters) { 206 207 // If everything is outside, then we stick with outside 208 int totalAction = SEND_MESSAGE_ACTION_DONT_SEND; 209 for (int i = 0; i < mFences.size(); i++) { 210 CbGeoUtils.Geometry fence = mFences.get(i); 211 @SendMessageAction 212 final int action = calculateSingleFence(coordinate, accuracyMeters, fence); 213 214 if (action == SEND_MESSAGE_ACTION_SEND) { 215 // The send action means we always go for it. 216 return action; 217 } else if (action == SEND_MESSAGE_ACTION_AMBIGUOUS) { 218 // If we are outside a geo, but then find that the accuracies overlap, 219 // we stick to overlap while still seeing if there are any cases where we are 220 // inside 221 totalAction = SEND_MESSAGE_ACTION_AMBIGUOUS; 222 } 223 } 224 return totalAction; 225 } 226 227 @SendMessageAction calculateSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, CbGeoUtils.Geometry fence)228 private int calculateSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, 229 CbGeoUtils.Geometry fence) { 230 if (fence.contains(coordinate)) { 231 return SEND_MESSAGE_ACTION_SEND; 232 } 233 234 if (mDoNewWay) { 235 return calculateSysSingleFence(coordinate, accuracyMeters, fence); 236 } else { 237 return SEND_MESSAGE_ACTION_DONT_SEND; 238 } 239 } 240 calculateSysSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, CbGeoUtils.Geometry fence)241 private int calculateSysSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, 242 CbGeoUtils.Geometry fence) { 243 Optional<Double> maybeDistance = 244 com.android.cellbroadcastservice.CbGeoUtils.distance(fence, coordinate); 245 if (!maybeDistance.isPresent()) { 246 return SEND_MESSAGE_ACTION_DONT_SEND; 247 } 248 249 double distance = maybeDistance.get(); 250 if (accuracyMeters <= mThresholdMeters && distance <= mThresholdMeters) { 251 // The accuracy is precise and we are within the threshold of the boundary, send 252 return SEND_MESSAGE_ACTION_SEND; 253 } 254 255 if (distance <= accuracyMeters) { 256 // Ambiguous case 257 return SEND_MESSAGE_ACTION_AMBIGUOUS; 258 } else { 259 return SEND_MESSAGE_ACTION_DONT_SEND; 260 } 261 } 262 } 263