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 */ calculatePersistentAction(CbGeoUtils.LatLng coordinate, float accuracyMeters)174 private void calculatePersistentAction(CbGeoUtils.LatLng coordinate, float accuracyMeters) { 175 // If we already marked this as a send, we don't need to check anything. 176 if (this.mAction != SEND_MESSAGE_ACTION_SEND) { 177 @SendMessageAction int newAction = 178 calculateActionFromFences(coordinate, accuracyMeters); 179 180 if (newAction == SEND_MESSAGE_ACTION_SEND) { 181 /* If the new action is in SEND, it doesn't matter what the old action is is. */ 182 this.mAction = newAction; 183 } else if (mAction != SEND_MESSAGE_ACTION_DONT_SEND) { 184 /* If the old action is in DONT_SEND, then always overwrite it with ambiguous. */ 185 this.mAction = newAction; 186 } else { 187 /* No-op because if we are in a don't send state, we don't want to overwrite 188 with an ambiguous state. */ 189 } 190 } 191 } 192 193 /** 194 * Calculates the proposed action state from the fences according to the rules: 195 * 1. Any coordinate with a SEND always wins. 196 * 2. If a coordinate \ accuracy overlaps any fence, go with AMBIGUOUS. 197 * 3. Otherwise, the coordinate is very far outside every fence and we move to DONT_SEND. 198 * @param coordinate the geo location 199 * @param accuracyMeters the accuracy from location manager 200 * @return the action 201 */ 202 @SendMessageAction calculateActionFromFences(CbGeoUtils.LatLng coordinate, float accuracyMeters)203 private int calculateActionFromFences(CbGeoUtils.LatLng coordinate, float accuracyMeters) { 204 205 // If everything is outside, then we stick with outside 206 int totalAction = SEND_MESSAGE_ACTION_DONT_SEND; 207 for (int i = 0; i < mFences.size(); i++) { 208 CbGeoUtils.Geometry fence = mFences.get(i); 209 @SendMessageAction 210 final int action = calculateSingleFence(coordinate, accuracyMeters, fence); 211 212 if (action == SEND_MESSAGE_ACTION_SEND) { 213 // The send action means we always go for it. 214 return action; 215 } else if (action == SEND_MESSAGE_ACTION_AMBIGUOUS) { 216 // If we are outside a geo, but then find that the accuracies overlap, 217 // we stick to overlap while still seeing if there are any cases where we are 218 // inside 219 totalAction = SEND_MESSAGE_ACTION_AMBIGUOUS; 220 } 221 } 222 return totalAction; 223 } 224 225 @SendMessageAction calculateSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, CbGeoUtils.Geometry fence)226 private int calculateSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, 227 CbGeoUtils.Geometry fence) { 228 if (fence.contains(coordinate)) { 229 return SEND_MESSAGE_ACTION_SEND; 230 } 231 232 if (mDoNewWay) { 233 return calculateSysSingleFence(coordinate, accuracyMeters, fence); 234 } else { 235 return SEND_MESSAGE_ACTION_DONT_SEND; 236 } 237 } 238 calculateSysSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, CbGeoUtils.Geometry fence)239 private int calculateSysSingleFence(CbGeoUtils.LatLng coordinate, float accuracyMeters, 240 CbGeoUtils.Geometry fence) { 241 Optional<Double> maybeDistance = 242 com.android.cellbroadcastservice.CbGeoUtils.distance(fence, coordinate); 243 if (!maybeDistance.isPresent()) { 244 return SEND_MESSAGE_ACTION_DONT_SEND; 245 } 246 247 double distance = maybeDistance.get(); 248 if (accuracyMeters <= mThresholdMeters && distance <= mThresholdMeters) { 249 // The accuracy is precise and we are within the threshold of the boundary, send 250 return SEND_MESSAGE_ACTION_SEND; 251 } 252 253 if (distance <= accuracyMeters) { 254 // Ambiguous case 255 return SEND_MESSAGE_ACTION_AMBIGUOUS; 256 } else { 257 return SEND_MESSAGE_ACTION_DONT_SEND; 258 } 259 } 260 } 261