1 /* 2 * Copyright (C) 2017 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.incallui.calllocation.impl; 18 19 import android.content.Context; 20 import android.location.Location; 21 import android.net.ConnectivityManager; 22 import android.net.NetworkInfo; 23 import android.os.Handler; 24 import android.support.annotation.IntDef; 25 import android.support.annotation.MainThread; 26 import android.support.v4.os.UserManagerCompat; 27 import com.android.dialer.common.Assert; 28 import com.android.dialer.common.LogUtil; 29 import com.android.dialer.util.PermissionsUtil; 30 import com.google.android.gms.location.FusedLocationProviderClient; 31 import com.google.android.gms.location.LocationListener; 32 import com.google.android.gms.location.LocationRequest; 33 import com.google.android.gms.location.LocationServices; 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** Uses the Fused location service to get location and pass updates on to listeners. */ 40 public class LocationHelper { 41 42 private static final int FAST_MIN_UPDATE_INTERVAL_MS = 5 * 1000; 43 private static final int SLOW_MIN_UPDATE_INTERVAL_MS = 30 * 1000; 44 private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000; 45 private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100; 46 47 public static final int LOCATION_STATUS_UNKNOWN = 0; 48 public static final int LOCATION_STATUS_OK = 1; 49 public static final int LOCATION_STATUS_STALE = 2; 50 public static final int LOCATION_STATUS_INACCURATE = 3; 51 public static final int LOCATION_STATUS_NO_LOCATION = 4; 52 53 /** Possible return values for {@code checkLocation()} */ 54 @IntDef({ 55 LOCATION_STATUS_UNKNOWN, 56 LOCATION_STATUS_OK, 57 LOCATION_STATUS_STALE, 58 LOCATION_STATUS_INACCURATE, 59 LOCATION_STATUS_NO_LOCATION 60 }) 61 @Retention(RetentionPolicy.SOURCE) 62 public @interface LocationStatus {} 63 64 private final LocationHelperInternal locationHelperInternal; 65 private final List<LocationListener> listeners = new ArrayList<>(); 66 67 @MainThread 68 LocationHelper(Context context) { 69 Assert.isMainThread(); 70 Assert.checkArgument(canGetLocation(context)); 71 locationHelperInternal = new LocationHelperInternal(context); 72 } 73 74 static boolean canGetLocation(Context context) { 75 if (!PermissionsUtil.hasLocationPermissions(context)) { 76 LogUtil.i("LocationHelper.canGetLocation", "no location permissions."); 77 return false; 78 } 79 80 // Ensure that both system location setting is on and google location services are enabled. 81 if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context) 82 || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) { 83 LogUtil.i("LocationHelper.canGetLocation", "location service is disabled."); 84 return false; 85 } 86 87 if (!UserManagerCompat.isUserUnlocked(context)) { 88 LogUtil.i("LocationHelper.canGetLocation", "location unavailable in FBE mode."); 89 return false; 90 } 91 92 return true; 93 } 94 95 /** 96 * Check whether the location is valid. We consider it valid if it was recorded within the 97 * specified time threshold of the present and has an accuracy less than the specified distance 98 * threshold. 99 * 100 * @param location The location to determine the validity of. 101 * @return {@code LocationStatus} indicating if the location is valid or the reason its not valid 102 */ 103 static @LocationStatus int checkLocation(Location location) { 104 if (location == null) { 105 LogUtil.i("LocationHelper.checkLocation", "no location"); 106 return LOCATION_STATUS_NO_LOCATION; 107 } 108 109 long locationTimeMs = location.getTime(); 110 long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs; 111 if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) { 112 LogUtil.i("LocationHelper.checkLocation", "stale location, age: " + elapsedTimeMs); 113 return LOCATION_STATUS_STALE; 114 } 115 116 if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) { 117 LogUtil.i("LocationHelper.checkLocation", "poor accuracy: " + location.getAccuracy()); 118 return LOCATION_STATUS_INACCURATE; 119 } 120 121 return LOCATION_STATUS_OK; 122 } 123 124 @MainThread 125 void addLocationListener(LocationListener listener) { 126 Assert.isMainThread(); 127 listeners.add(listener); 128 } 129 130 @MainThread 131 void removeLocationListener(LocationListener listener) { 132 Assert.isMainThread(); 133 listeners.remove(listener); 134 } 135 136 @MainThread 137 void close() { 138 Assert.isMainThread(); 139 LogUtil.enterBlock("LocationHelper.close"); 140 listeners.clear(); 141 locationHelperInternal.close(); 142 } 143 144 @MainThread 145 void onLocationChanged(Location location, boolean isConnected) { 146 Assert.isMainThread(); 147 LogUtil.i("LocationHelper.onLocationChanged", "location: " + location); 148 149 for (LocationListener listener : listeners) { 150 listener.onLocationChanged(location); 151 } 152 } 153 154 /** 155 * This class contains all the asynchronous callbacks. It only posts location changes back to the 156 * outer class on the main thread. 157 */ 158 private class LocationHelperInternal implements LocationListener { 159 160 private final FusedLocationProviderClient locationClient; 161 private final ConnectivityManager connectivityManager; 162 private final Handler mainThreadHandler = new Handler(); 163 private boolean gotGoodLocation; 164 165 @MainThread 166 LocationHelperInternal(Context context) { 167 Assert.isMainThread(); 168 locationClient = LocationServices.getFusedLocationProviderClient(context); 169 connectivityManager = 170 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 171 requestUpdates(); 172 getLocation(); 173 } 174 175 void close() { 176 LogUtil.enterBlock("LocationHelperInternal.close"); 177 locationClient.removeLocationUpdates(this); 178 } 179 180 private void requestUpdates() { 181 LogUtil.enterBlock("LocationHelperInternal.requestUpdates"); 182 183 int interval = gotGoodLocation ? SLOW_MIN_UPDATE_INTERVAL_MS : FAST_MIN_UPDATE_INTERVAL_MS; 184 LocationRequest locationRequest = 185 LocationRequest.create() 186 .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) 187 .setInterval(interval) 188 .setFastestInterval(interval); 189 190 locationClient 191 .requestLocationUpdates(locationRequest, this) 192 .addOnSuccessListener( 193 result -> LogUtil.i("LocationHelperInternal.requestUpdates", "onSuccess")) 194 .addOnFailureListener( 195 e -> LogUtil.e("LocationHelperInternal.requestUpdates", "onFailure", e)); 196 } 197 198 private void getLocation() { 199 LogUtil.enterBlock("LocationHelperInternal.getLocation"); 200 201 locationClient 202 .getLastLocation() 203 .addOnSuccessListener( 204 location -> { 205 LogUtil.i("LocationHelperInternal.getLocation", "onSuccess"); 206 Assert.isMainThread(); 207 LocationHelper.this.onLocationChanged(location, isConnected()); 208 maybeAdjustUpdateInterval(location); 209 }) 210 .addOnFailureListener( 211 e -> LogUtil.e("LocationHelperInternal.getLocation", "onFailure", e)); 212 } 213 214 @Override 215 public void onLocationChanged(Location location) { 216 // Post new location on main thread 217 mainThreadHandler.post( 218 new Runnable() { 219 @Override 220 public void run() { 221 LocationHelper.this.onLocationChanged(location, isConnected()); 222 maybeAdjustUpdateInterval(location); 223 } 224 }); 225 } 226 227 private void maybeAdjustUpdateInterval(Location location) { 228 if (!gotGoodLocation && checkLocation(location) == LOCATION_STATUS_OK) { 229 LogUtil.i("LocationHelperInternal.maybeAdjustUpdateInterval", "got good location"); 230 gotGoodLocation = true; 231 requestUpdates(); 232 } 233 } 234 235 /** @return Whether the phone is connected to data. */ 236 private boolean isConnected() { 237 if (connectivityManager == null) { 238 return false; 239 } 240 NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); 241 return networkInfo != null && networkInfo.isConnectedOrConnecting(); 242 } 243 } 244 } 245