/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.incallui.calllocation.impl; import android.content.Context; import android.location.Location; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Handler; import android.support.annotation.IntDef; import android.support.annotation.MainThread; import android.support.v4.os.UserManagerCompat; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.util.PermissionsUtil; import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** Uses the Fused location service to get location and pass updates on to listeners. */ public class LocationHelper { private static final int FAST_MIN_UPDATE_INTERVAL_MS = 5 * 1000; private static final int SLOW_MIN_UPDATE_INTERVAL_MS = 30 * 1000; private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000; private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100; public static final int LOCATION_STATUS_UNKNOWN = 0; public static final int LOCATION_STATUS_OK = 1; public static final int LOCATION_STATUS_STALE = 2; public static final int LOCATION_STATUS_INACCURATE = 3; public static final int LOCATION_STATUS_NO_LOCATION = 4; /** Possible return values for {@code checkLocation()} */ @IntDef({ LOCATION_STATUS_UNKNOWN, LOCATION_STATUS_OK, LOCATION_STATUS_STALE, LOCATION_STATUS_INACCURATE, LOCATION_STATUS_NO_LOCATION }) @Retention(RetentionPolicy.SOURCE) public @interface LocationStatus {} private final LocationHelperInternal locationHelperInternal; private final List listeners = new ArrayList<>(); @MainThread LocationHelper(Context context) { Assert.isMainThread(); Assert.checkArgument(canGetLocation(context)); locationHelperInternal = new LocationHelperInternal(context); } static boolean canGetLocation(Context context) { if (!PermissionsUtil.hasLocationPermissions(context)) { LogUtil.i("LocationHelper.canGetLocation", "no location permissions."); return false; } // Ensure that both system location setting is on and google location services are enabled. if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context) || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) { LogUtil.i("LocationHelper.canGetLocation", "location service is disabled."); return false; } if (!UserManagerCompat.isUserUnlocked(context)) { LogUtil.i("LocationHelper.canGetLocation", "location unavailable in FBE mode."); return false; } return true; } /** * Check whether the location is valid. We consider it valid if it was recorded within the * specified time threshold of the present and has an accuracy less than the specified distance * threshold. * * @param location The location to determine the validity of. * @return {@code LocationStatus} indicating if the location is valid or the reason its not valid */ static @LocationStatus int checkLocation(Location location) { if (location == null) { LogUtil.i("LocationHelper.checkLocation", "no location"); return LOCATION_STATUS_NO_LOCATION; } long locationTimeMs = location.getTime(); long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs; if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) { LogUtil.i("LocationHelper.checkLocation", "stale location, age: " + elapsedTimeMs); return LOCATION_STATUS_STALE; } if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) { LogUtil.i("LocationHelper.checkLocation", "poor accuracy: " + location.getAccuracy()); return LOCATION_STATUS_INACCURATE; } return LOCATION_STATUS_OK; } @MainThread void addLocationListener(LocationListener listener) { Assert.isMainThread(); listeners.add(listener); } @MainThread void removeLocationListener(LocationListener listener) { Assert.isMainThread(); listeners.remove(listener); } @MainThread void close() { Assert.isMainThread(); LogUtil.enterBlock("LocationHelper.close"); listeners.clear(); locationHelperInternal.close(); } @MainThread void onLocationChanged(Location location, boolean isConnected) { Assert.isMainThread(); LogUtil.i("LocationHelper.onLocationChanged", "location: " + location); for (LocationListener listener : listeners) { listener.onLocationChanged(location); } } /** * This class contains all the asynchronous callbacks. It only posts location changes back to the * outer class on the main thread. */ private class LocationHelperInternal implements LocationListener { private final FusedLocationProviderClient locationClient; private final ConnectivityManager connectivityManager; private final Handler mainThreadHandler = new Handler(); private boolean gotGoodLocation; @MainThread LocationHelperInternal(Context context) { Assert.isMainThread(); locationClient = LocationServices.getFusedLocationProviderClient(context); connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); requestUpdates(); getLocation(); } void close() { LogUtil.enterBlock("LocationHelperInternal.close"); locationClient.removeLocationUpdates(this); } private void requestUpdates() { LogUtil.enterBlock("LocationHelperInternal.requestUpdates"); int interval = gotGoodLocation ? SLOW_MIN_UPDATE_INTERVAL_MS : FAST_MIN_UPDATE_INTERVAL_MS; LocationRequest locationRequest = LocationRequest.create() .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) .setInterval(interval) .setFastestInterval(interval); locationClient .requestLocationUpdates(locationRequest, this) .addOnSuccessListener( result -> LogUtil.i("LocationHelperInternal.requestUpdates", "onSuccess")) .addOnFailureListener( e -> LogUtil.e("LocationHelperInternal.requestUpdates", "onFailure", e)); } private void getLocation() { LogUtil.enterBlock("LocationHelperInternal.getLocation"); locationClient .getLastLocation() .addOnSuccessListener( location -> { LogUtil.i("LocationHelperInternal.getLocation", "onSuccess"); Assert.isMainThread(); LocationHelper.this.onLocationChanged(location, isConnected()); maybeAdjustUpdateInterval(location); }) .addOnFailureListener( e -> LogUtil.e("LocationHelperInternal.getLocation", "onFailure", e)); } @Override public void onLocationChanged(Location location) { // Post new location on main thread mainThreadHandler.post( new Runnable() { @Override public void run() { LocationHelper.this.onLocationChanged(location, isConnected()); maybeAdjustUpdateInterval(location); } }); } private void maybeAdjustUpdateInterval(Location location) { if (!gotGoodLocation && checkLocation(location) == LOCATION_STATUS_OK) { LogUtil.i("LocationHelperInternal.maybeAdjustUpdateInterval", "got good location"); gotGoodLocation = true; requestUpdates(); } } /** @return Whether the phone is connected to data. */ private boolean isConnected() { if (connectivityManager == null) { return false; } NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnectedOrConnecting(); } } }