| /* |
| * Copyright (C) 2012 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.location.fused; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.HashMap; |
| |
| import com.android.location.provider.LocationProviderBase; |
| import com.android.location.provider.LocationRequestUnbundled; |
| import com.android.location.provider.ProviderRequestUnbundled; |
| |
| import android.content.Context; |
| import android.location.Location; |
| import android.location.LocationListener; |
| import android.location.LocationManager; |
| import android.os.Bundle; |
| import android.os.Looper; |
| import android.os.Parcelable; |
| import android.os.SystemClock; |
| import android.os.WorkSource; |
| import android.util.Log; |
| |
| public class FusionEngine implements LocationListener { |
| public interface Callback { |
| public void reportLocation(Location location); |
| } |
| |
| private static final String TAG = "FusedLocation"; |
| private static final String NETWORK = LocationManager.NETWORK_PROVIDER; |
| private static final String GPS = LocationManager.GPS_PROVIDER; |
| private static final String FUSED = LocationProviderBase.FUSED_PROVIDER; |
| |
| public static final long SWITCH_ON_FRESHNESS_CLIFF_NS = 11 * 1000000000; // 11 seconds |
| |
| private final Context mContext; |
| private final LocationManager mLocationManager; |
| private final Looper mLooper; |
| |
| // all fields are only used on mLooper thread. except for in dump() which is not thread-safe |
| private Callback mCallback; |
| private Location mFusedLocation; |
| private Location mGpsLocation; |
| private Location mNetworkLocation; |
| |
| private boolean mEnabled; |
| private ProviderRequestUnbundled mRequest; |
| |
| private final HashMap<String, ProviderStats> mStats = new HashMap<String, ProviderStats>(); |
| |
| public FusionEngine(Context context, Looper looper) { |
| mContext = context; |
| mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); |
| mNetworkLocation = new Location(""); |
| mNetworkLocation.setAccuracy(Float.MAX_VALUE); |
| mGpsLocation = new Location(""); |
| mGpsLocation.setAccuracy(Float.MAX_VALUE); |
| mLooper = looper; |
| |
| mStats.put(GPS, new ProviderStats()); |
| mStats.get(GPS).available = mLocationManager.isProviderEnabled(GPS); |
| mStats.put(NETWORK, new ProviderStats()); |
| mStats.get(NETWORK).available = mLocationManager.isProviderEnabled(NETWORK); |
| |
| } |
| |
| public void init(Callback callback) { |
| Log.i(TAG, "engine started (" + mContext.getPackageName() + ")"); |
| mCallback = callback; |
| } |
| |
| /** |
| * Called to stop doing any work, and release all resources |
| * This can happen when a better fusion engine is installed |
| * in a different package, and this one is no longer needed. |
| * Called on mLooper thread |
| */ |
| public void deinit() { |
| mRequest = null; |
| disable(); |
| Log.i(TAG, "engine stopped (" + mContext.getPackageName() + ")"); |
| } |
| |
| /** Called on mLooper thread */ |
| public void enable() { |
| mEnabled = true; |
| updateRequirements(); |
| } |
| |
| /** Called on mLooper thread */ |
| public void disable() { |
| mEnabled = false; |
| updateRequirements(); |
| } |
| |
| /** Called on mLooper thread */ |
| public void setRequest(ProviderRequestUnbundled request, WorkSource source) { |
| mRequest = request; |
| mEnabled = request.getReportLocation(); |
| updateRequirements(); |
| } |
| |
| private static class ProviderStats { |
| public boolean available; |
| public boolean requested; |
| public long requestTime; |
| public long minTime; |
| @Override |
| public String toString() { |
| StringBuilder s = new StringBuilder(); |
| s.append(available ? "AVAILABLE" : "UNAVAILABLE"); |
| s.append(requested ? " REQUESTED" : " ---"); |
| return s.toString(); |
| } |
| } |
| |
| private void enableProvider(String name, long minTime) { |
| ProviderStats stats = mStats.get(name); |
| |
| if (!stats.requested) { |
| stats.requestTime = SystemClock.elapsedRealtime(); |
| stats.requested = true; |
| stats.minTime = minTime; |
| mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); |
| } else if (stats.minTime != minTime) { |
| stats.minTime = minTime; |
| mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); |
| } |
| } |
| |
| private void disableProvider(String name) { |
| ProviderStats stats = mStats.get(name); |
| |
| if (stats.requested) { |
| stats.requested = false; |
| mLocationManager.removeUpdates(this); //TODO GLOBAL |
| } |
| } |
| |
| private void updateRequirements() { |
| if (mEnabled == false || mRequest == null) { |
| mRequest = null; |
| disableProvider(NETWORK); |
| disableProvider(GPS); |
| return; |
| } |
| |
| long networkInterval = Long.MAX_VALUE; |
| long gpsInterval = Long.MAX_VALUE; |
| for (LocationRequestUnbundled request : mRequest.getLocationRequests()) { |
| switch (request.getQuality()) { |
| case LocationRequestUnbundled.ACCURACY_FINE: |
| case LocationRequestUnbundled.POWER_HIGH: |
| if (request.getInterval() < gpsInterval) { |
| gpsInterval = request.getInterval(); |
| } |
| if (request.getInterval() < networkInterval) { |
| networkInterval = request.getInterval(); |
| } |
| break; |
| case LocationRequestUnbundled.ACCURACY_BLOCK: |
| case LocationRequestUnbundled.ACCURACY_CITY: |
| case LocationRequestUnbundled.POWER_LOW: |
| if (request.getInterval() < networkInterval) { |
| networkInterval = request.getInterval(); |
| } |
| break; |
| } |
| } |
| |
| if (gpsInterval < Long.MAX_VALUE) { |
| enableProvider(GPS, gpsInterval); |
| } else { |
| disableProvider(GPS); |
| } |
| if (networkInterval < Long.MAX_VALUE) { |
| enableProvider(NETWORK, networkInterval); |
| } else { |
| disableProvider(NETWORK); |
| } |
| } |
| |
| /** |
| * Test whether one location (a) is better to use than another (b). |
| */ |
| private static boolean isBetterThan(Location locationA, Location locationB) { |
| if (locationA == null) { |
| return false; |
| } |
| if (locationB == null) { |
| return true; |
| } |
| // A provider is better if the reading is sufficiently newer. Heading |
| // underground can cause GPS to stop reporting fixes. In this case it's |
| // appropriate to revert to cell, even when its accuracy is less. |
| if (locationA.getElapsedRealtimeNanos() > locationB.getElapsedRealtimeNanos() + SWITCH_ON_FRESHNESS_CLIFF_NS) { |
| return true; |
| } |
| |
| // A provider is better if it has better accuracy. Assuming both readings |
| // are fresh (and by that accurate), choose the one with the smaller |
| // accuracy circle. |
| if (!locationA.hasAccuracy()) { |
| return false; |
| } |
| if (!locationB.hasAccuracy()) { |
| return true; |
| } |
| return locationA.getAccuracy() < locationB.getAccuracy(); |
| } |
| |
| private void updateFusedLocation() { |
| // may the best location win! |
| if (isBetterThan(mGpsLocation, mNetworkLocation)) { |
| mFusedLocation = new Location(mGpsLocation); |
| } else { |
| mFusedLocation = new Location(mNetworkLocation); |
| } |
| mFusedLocation.setProvider(FUSED); |
| if (mNetworkLocation != null) { |
| // copy NO_GPS_LOCATION extra from mNetworkLocation into mFusedLocation |
| Bundle srcExtras = mNetworkLocation.getExtras(); |
| if (srcExtras != null) { |
| Parcelable srcParcelable = |
| srcExtras.getParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION); |
| if (srcParcelable instanceof Location) { |
| Bundle dstExtras = mFusedLocation.getExtras(); |
| if (dstExtras == null) { |
| dstExtras = new Bundle(); |
| mFusedLocation.setExtras(dstExtras); |
| } |
| dstExtras.putParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION, |
| (Location) srcParcelable); |
| } |
| } |
| } |
| |
| if (mCallback != null) { |
| mCallback.reportLocation(mFusedLocation); |
| } else { |
| Log.w(TAG, "Location updates received while fusion engine not started"); |
| } |
| } |
| |
| /** Called on mLooper thread */ |
| @Override |
| public void onLocationChanged(Location location) { |
| if (GPS.equals(location.getProvider())) { |
| mGpsLocation = location; |
| updateFusedLocation(); |
| } else if (NETWORK.equals(location.getProvider())) { |
| mNetworkLocation = location; |
| updateFusedLocation(); |
| } |
| } |
| |
| /** Called on mLooper thread */ |
| @Override |
| public void onStatusChanged(String provider, int status, Bundle extras) { } |
| |
| /** Called on mLooper thread */ |
| @Override |
| public void onProviderEnabled(String provider) { |
| ProviderStats stats = mStats.get(provider); |
| if (stats == null) return; |
| |
| stats.available = true; |
| } |
| |
| /** Called on mLooper thread */ |
| @Override |
| public void onProviderDisabled(String provider) { |
| ProviderStats stats = mStats.get(provider); |
| if (stats == null) return; |
| |
| stats.available = false; |
| } |
| |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| StringBuilder s = new StringBuilder(); |
| s.append("mEnabled=" + mEnabled).append(' ').append(mRequest).append('\n'); |
| s.append("fused=").append(mFusedLocation).append('\n'); |
| s.append(String.format("gps %s\n", mGpsLocation)); |
| s.append(" ").append(mStats.get(GPS)).append('\n'); |
| s.append(String.format("net %s\n", mNetworkLocation)); |
| s.append(" ").append(mStats.get(NETWORK)).append('\n'); |
| pw.append(s); |
| } |
| } |