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