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