Home | History | Annotate | Download | only in fused
      1 /*
      2  * Copyright (C) 2012 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.location.fused;
     18 
     19 import java.io.FileDescriptor;
     20 import java.io.PrintWriter;
     21 import java.util.HashMap;
     22 
     23 import com.android.location.provider.LocationProviderBase;
     24 import com.android.location.provider.LocationRequestUnbundled;
     25 import com.android.location.provider.ProviderRequestUnbundled;
     26 
     27 import android.content.Context;
     28 import android.location.Location;
     29 import android.location.LocationListener;
     30 import android.location.LocationManager;
     31 import android.os.Bundle;
     32 import android.os.Looper;
     33 import android.os.Parcelable;
     34 import android.os.SystemClock;
     35 import android.os.WorkSource;
     36 import android.util.Log;
     37 
     38 public class FusionEngine implements LocationListener {
     39     public interface Callback {
     40         public void reportLocation(Location location);
     41     }
     42 
     43     private static final String TAG = "FusedLocation";
     44     private static final String NETWORK = LocationManager.NETWORK_PROVIDER;
     45     private static final String GPS = LocationManager.GPS_PROVIDER;
     46     private static final String FUSED = LocationProviderBase.FUSED_PROVIDER;
     47 
     48     public static final long SWITCH_ON_FRESHNESS_CLIFF_NS = 11 * 1000000000; // 11 seconds
     49 
     50     private final Context mContext;
     51     private final LocationManager mLocationManager;
     52     private final Looper mLooper;
     53 
     54     // all fields are only used on mLooper thread. except for in dump() which is not thread-safe
     55     private Callback mCallback;
     56     private Location mFusedLocation;
     57     private Location mGpsLocation;
     58     private Location mNetworkLocation;
     59 
     60     private boolean mEnabled;
     61     private ProviderRequestUnbundled mRequest;
     62 
     63     private final HashMap<String, ProviderStats> mStats = new HashMap<String, ProviderStats>();
     64 
     65     public FusionEngine(Context context, Looper looper) {
     66         mContext = context;
     67         mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
     68         mNetworkLocation = new Location("");
     69         mNetworkLocation.setAccuracy(Float.MAX_VALUE);
     70         mGpsLocation = new Location("");
     71         mGpsLocation.setAccuracy(Float.MAX_VALUE);
     72         mLooper = looper;
     73 
     74         mStats.put(GPS, new ProviderStats());
     75         mStats.get(GPS).available = mLocationManager.isProviderEnabled(GPS);
     76         mStats.put(NETWORK, new ProviderStats());
     77         mStats.get(NETWORK).available = mLocationManager.isProviderEnabled(NETWORK);
     78 
     79     }
     80 
     81     public void init(Callback callback) {
     82         Log.i(TAG, "engine started (" + mContext.getPackageName() + ")");
     83         mCallback = callback;
     84     }
     85 
     86     /**
     87      * Called to stop doing any work, and release all resources
     88      * This can happen when a better fusion engine is installed
     89      * in a different package, and this one is no longer needed.
     90      * Called on mLooper thread
     91      */
     92     public void deinit() {
     93         mRequest = null;
     94         disable();
     95         Log.i(TAG, "engine stopped (" + mContext.getPackageName() + ")");
     96     }
     97 
     98     /** Called on mLooper thread */
     99     public void enable() {
    100         if (!mEnabled) {
    101             mEnabled = true;
    102             updateRequirements();
    103         }
    104     }
    105 
    106     /** Called on mLooper thread */
    107     public void disable() {
    108         if (mEnabled) {
    109             mEnabled = false;
    110             updateRequirements();
    111         }
    112     }
    113 
    114     /** Called on mLooper thread */
    115     public void setRequest(ProviderRequestUnbundled request, WorkSource source) {
    116         mRequest = request;
    117         mEnabled = request.getReportLocation();
    118         updateRequirements();
    119     }
    120 
    121     private static class ProviderStats {
    122         public boolean available;
    123         public boolean requested;
    124         public long requestTime;
    125         public long minTime;
    126         @Override
    127         public String toString() {
    128             StringBuilder s = new StringBuilder();
    129             s.append(available ? "AVAILABLE" : "UNAVAILABLE");
    130             s.append(requested ? " REQUESTED" : " ---");
    131             return s.toString();
    132         }
    133     }
    134 
    135     private void enableProvider(String name, long minTime) {
    136         ProviderStats stats = mStats.get(name);
    137 
    138         if (!stats.requested) {
    139             stats.requestTime = SystemClock.elapsedRealtime();
    140             stats.requested = true;
    141             stats.minTime = minTime;
    142             mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
    143         } else if (stats.minTime != minTime) {
    144             stats.minTime = minTime;
    145             mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
    146         }
    147     }
    148 
    149     private void disableProvider(String name) {
    150         ProviderStats stats = mStats.get(name);
    151 
    152         if (stats.requested) {
    153             stats.requested = false;
    154             mLocationManager.removeUpdates(this);  //TODO GLOBAL
    155         }
    156     }
    157 
    158     private void updateRequirements() {
    159         if (mEnabled == false || mRequest == null) {
    160             mRequest = null;
    161             disableProvider(NETWORK);
    162             disableProvider(GPS);
    163             return;
    164         }
    165 
    166         long networkInterval = Long.MAX_VALUE;
    167         long gpsInterval = Long.MAX_VALUE;
    168         for (LocationRequestUnbundled request : mRequest.getLocationRequests()) {
    169             switch (request.getQuality()) {
    170                 case LocationRequestUnbundled.ACCURACY_FINE:
    171                 case LocationRequestUnbundled.POWER_HIGH:
    172                     if (request.getInterval() < gpsInterval) {
    173                         gpsInterval = request.getInterval();
    174                     }
    175                     if (request.getInterval() < networkInterval) {
    176                         networkInterval = request.getInterval();
    177                     }
    178                     break;
    179                 case LocationRequestUnbundled.ACCURACY_BLOCK:
    180                 case LocationRequestUnbundled.ACCURACY_CITY:
    181                 case LocationRequestUnbundled.POWER_LOW:
    182                     if (request.getInterval() < networkInterval) {
    183                         networkInterval = request.getInterval();
    184                     }
    185                     break;
    186             }
    187         }
    188 
    189         if (gpsInterval < Long.MAX_VALUE) {
    190             enableProvider(GPS, gpsInterval);
    191         } else {
    192             disableProvider(GPS);
    193         }
    194         if (networkInterval < Long.MAX_VALUE) {
    195             enableProvider(NETWORK, networkInterval);
    196         } else {
    197             disableProvider(NETWORK);
    198         }
    199     }
    200 
    201     /**
    202      * Test whether one location (a) is better to use than another (b).
    203      */
    204     private static boolean isBetterThan(Location locationA, Location locationB) {
    205       if (locationA == null) {
    206         return false;
    207       }
    208       if (locationB == null) {
    209         return true;
    210       }
    211       // A provider is better if the reading is sufficiently newer.  Heading
    212       // underground can cause GPS to stop reporting fixes.  In this case it's
    213       // appropriate to revert to cell, even when its accuracy is less.
    214       if (locationA.getElapsedRealtimeNanos() > locationB.getElapsedRealtimeNanos() + SWITCH_ON_FRESHNESS_CLIFF_NS) {
    215         return true;
    216       }
    217 
    218       // A provider is better if it has better accuracy.  Assuming both readings
    219       // are fresh (and by that accurate), choose the one with the smaller
    220       // accuracy circle.
    221       if (!locationA.hasAccuracy()) {
    222         return false;
    223       }
    224       if (!locationB.hasAccuracy()) {
    225         return true;
    226       }
    227       return locationA.getAccuracy() < locationB.getAccuracy();
    228     }
    229 
    230     private void updateFusedLocation() {
    231         // may the best location win!
    232         if (isBetterThan(mGpsLocation, mNetworkLocation)) {
    233             mFusedLocation = new Location(mGpsLocation);
    234         } else {
    235             mFusedLocation = new Location(mNetworkLocation);
    236         }
    237         mFusedLocation.setProvider(FUSED);
    238         if (mNetworkLocation != null) {
    239             // copy NO_GPS_LOCATION extra from mNetworkLocation into mFusedLocation
    240             Bundle srcExtras = mNetworkLocation.getExtras();
    241             if (srcExtras != null) {
    242                 Parcelable srcParcelable =
    243                         srcExtras.getParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION);
    244                 if (srcParcelable instanceof Location) {
    245                     Bundle dstExtras = mFusedLocation.getExtras();
    246                     if (dstExtras == null) {
    247                         dstExtras = new Bundle();
    248                         mFusedLocation.setExtras(dstExtras);
    249                     }
    250                     dstExtras.putParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION,
    251                             (Location) srcParcelable);
    252                 }
    253             }
    254         }
    255 
    256         if (mCallback != null) {
    257           mCallback.reportLocation(mFusedLocation);
    258         } else {
    259           Log.w(TAG, "Location updates received while fusion engine not started");
    260         }
    261     }
    262 
    263     /** Called on mLooper thread */
    264     @Override
    265     public void onLocationChanged(Location location) {
    266         if (GPS.equals(location.getProvider())) {
    267             mGpsLocation = location;
    268             updateFusedLocation();
    269         } else if (NETWORK.equals(location.getProvider())) {
    270             mNetworkLocation = location;
    271             updateFusedLocation();
    272         }
    273     }
    274 
    275     /** Called on mLooper thread */
    276     @Override
    277     public void onStatusChanged(String provider, int status, Bundle extras) {  }
    278 
    279     /** Called on mLooper thread */
    280     @Override
    281     public void onProviderEnabled(String provider) {
    282         ProviderStats stats = mStats.get(provider);
    283         if (stats == null) return;
    284 
    285         stats.available = true;
    286     }
    287 
    288     /** Called on mLooper thread */
    289     @Override
    290     public void onProviderDisabled(String provider) {
    291         ProviderStats stats = mStats.get(provider);
    292         if (stats == null) return;
    293 
    294         stats.available = false;
    295     }
    296 
    297     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    298         StringBuilder s = new StringBuilder();
    299         s.append("mEnabled=" + mEnabled).append(' ').append(mRequest).append('\n');
    300         s.append("fused=").append(mFusedLocation).append('\n');
    301         s.append(String.format("gps %s\n", mGpsLocation));
    302         s.append("    ").append(mStats.get(GPS)).append('\n');
    303         s.append(String.format("net %s\n", mNetworkLocation));
    304         s.append("    ").append(mStats.get(NETWORK)).append('\n');
    305         pw.append(s);
    306     }
    307 
    308     /** Called on mLooper thread */
    309     public void switchUser() {
    310         // reset state to prevent location data leakage
    311         mFusedLocation = null;
    312         mGpsLocation = null;
    313         mNetworkLocation = null;
    314     }
    315 }
    316