Home | History | Annotate | Download | only in impl
      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