blob: 39f432f14b822f54967a41b211860baeba36e4d9 [file] [log] [blame]
/*
* Copyright (C) 2007 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.internal.location;
import com.android.internal.location.protocol.GDebugProfile;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import android.location.Location;
import android.net.wifi.ScanResult;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.Log;
/**
* Listens for GPS and cell/wifi changes and anonymously uploads to server for
* improving quality of service of NetworkLocationProvider. This service is only enabled when
* the user has enabled the network location provider.
*
* {@hide}
*/
public class LocationCollector {
private static final String TAG = "LocationCollector";
// last location valid for 12 minutes
private static final long MIN_VALID_LOCATION_TIME = 12 * 60 * 1000L;
// don't send wifi more than every 10 min
private static final long MIN_TIME_BETWEEN_WIFI_REPORTS = 10 * 60 * 1000L;
// atleast 5 changed APs for wifi collection
private static final int MIN_CHANGED_WIFI_POINTS = 5;
// don't collect if distance moved less than 200 meters
private static final int MIN_DISTANCE_BETWEEN_REPORTS = 200;
// don't collect if battery level less than 20%
private static final double MIN_BATTERY_LEVEL = 0.2;
// if battery level is greater than 90% and plugged in, collect more frequently
private static final double CHARGED_BATTERY_LEVEL = 0.9;
// collect bursts every 15 minutes (running on battery)
private static final long BURST_REST_TIME_ON_BATTERY = 15 * 60 * 1000L;
// collect bursts every 8 minutes (when plugged in)
private static final long BURST_REST_TIME_PLUGGED = 8 * 60 * 1000L;
// collect burst samples every 12 seconds
private static final int BURST_MEASUREMENT_INTERVAL = 12 * 1000;
// collect 11 burst samples before resting (11 samples every 12 seconds = 2 minute bursts)
private static final int BURST_NUM_SAMPLES = 11;
// don't collect bursts if user in same loc for 2 bursts
private static final int MAX_BURSTS_FROM_SAME_LOCATION = 2;
// don't send more than 2 bursts if user hasn't moved more than 25 meters
private static final int MIN_DISTANCE_BETWEEN_BURSTS = 25;
// Cell State
private CellState mCellState = null;
private CellUploads mCellUploads = new CellUploads();
// GPS state
private Location mLastKnownLocation = null;
private Location mLastUploadedLocation = null;
private long mLastKnownLocationTime = 0;
private long mLastUploadedLocationTime = 0;
// Burst state
private Location mLastBurstLocation = null;
private long mLastBurstEndTime = 0;
private long mCurrentBurstStartTime = 0;
private int mCurrentBurstNumSamples = 0;
private int mNumBurstsFromLastLocation = 0;
// WiFi state
private List<ScanResult> mWifiLastScanResults = null;
private List<ScanResult> mWifiCurrentScanResults = null;
private long mLastWifiScanElapsedTime = 0;
private long mLastWifiScanRealTime = 0;
private boolean mWifiUploadedWithoutLocation = false;
// Collection state
private boolean mNetworkProviderIsEnabled = true;
private boolean mBatteryLevelIsHealthy = true;
private boolean mBatteryChargedAndPlugged = false;
// Location masf service
private LocationMasfClient mMasfClient;
public LocationCollector(LocationMasfClient masfClient) {
mMasfClient = masfClient;
}
/**
* Updates cell tower state. This is usually always up to date so should be uploaded
* each time a new location is available.
*
* @param newState cell state
*/
public synchronized void updateCellState(CellState newState) {
if (newState == null) {
throw new IllegalArgumentException("cell state is null");
}
if (!newState.isValid()) {
return;
}
if (mCellState != null && mCellState.equals(newState)) {
return;
}
mCellState = newState;
log("updateCellState(): Updated to " + mCellState.getCid() + "," + mCellState.getLac());
if (isCollectionEnabled()) {
addToQueue(GDebugProfile.TRIGGER_CELL_CHANGE);
}
}
/**
* Updates GPS location if collection is enabled
*
* @param location location object
*/
public synchronized void updateLocation(Location location) {
// Don't do anything if collection is disabled
if (!isCollectionEnabled()) {
return;
}
long now = SystemClock.elapsedRealtime();
// Update last known location
if (mLastKnownLocation == null) {
mLastKnownLocation = new Location(location);
} else {
mLastKnownLocation.set(location);
}
mLastKnownLocationTime = now;
// Burst rest time depends on battery state
long restTime = BURST_REST_TIME_ON_BATTERY;
if (mBatteryChargedAndPlugged) {
restTime = BURST_REST_TIME_PLUGGED;
}
int trigger;
// In burst mode if either first burst or enough time has passed since last burst
if (mLastBurstEndTime == 0 || (now - mLastBurstEndTime > restTime)) {
// If location is too recent, then don't do anything!
if (now - mLastUploadedLocationTime < BURST_MEASUREMENT_INTERVAL) {
return;
}
int distanceFromLastBurst = -1;
if (mLastBurstLocation != null) {
distanceFromLastBurst = (int) mLastBurstLocation.distanceTo(location);
// Too many bursts from same location, don't upload
if (distanceFromLastBurst < MIN_DISTANCE_BETWEEN_BURSTS &&
mNumBurstsFromLastLocation >= MAX_BURSTS_FROM_SAME_LOCATION) {
log("NO UPLOAD: Too many bursts from same location.");
return;
}
}
if (mCurrentBurstStartTime == 0) {
// Start the burst!
mCurrentBurstStartTime = now;
mCurrentBurstNumSamples = 1;
trigger = GDebugProfile.TRIGGER_COLLECTION_START_BURST;
} else if (now - mCurrentBurstStartTime > restTime) {
// Burst got old, start a new one
mCurrentBurstStartTime = now;
mCurrentBurstNumSamples = 1;
trigger = GDebugProfile.TRIGGER_COLLECTION_RESTART_BURST;
} else if (mCurrentBurstNumSamples == BURST_NUM_SAMPLES - 1) {
// Finished a burst
mLastBurstEndTime = now;
mCurrentBurstStartTime = 0;
mCurrentBurstNumSamples = 0;
// Make sure we don't upload too many bursts from same location
if (mLastBurstLocation == null) {
mLastBurstLocation = new Location(location);
mNumBurstsFromLastLocation = 1;
trigger = GDebugProfile.TRIGGER_COLLECTION_END_BURST;
} else {
if (distanceFromLastBurst != -1 &&
distanceFromLastBurst < MIN_DISTANCE_BETWEEN_BURSTS) {
// User hasnt moved much from last location, keep track of count,
// don't update last burst loc
mNumBurstsFromLastLocation++;
trigger = GDebugProfile.TRIGGER_COLLECTION_END_BURST_AT_SAME_LOCATION;
} else {
// User has moved enough, update last burst loc
mLastBurstLocation.set(location);
mNumBurstsFromLastLocation = 1;
trigger = GDebugProfile.TRIGGER_COLLECTION_END_BURST;
}
}
} else {
// Increment burst sample count
mCurrentBurstNumSamples++;
trigger = GDebugProfile.TRIGGER_COLLECTION_CONTINUE_BURST;
}
} else if (mLastUploadedLocation != null
&& (mLastUploadedLocation.distanceTo(location) > MIN_DISTANCE_BETWEEN_REPORTS)) {
// If not in burst mode but has moved a reasonable distance, upload!
trigger = GDebugProfile.TRIGGER_COLLECTION_MOVED_DISTANCE;
} else {
// Not in burst mode or hasn't moved enough
log("NO UPLOAD: Not in burst or moving mode. Resting for " + restTime + " ms");
return;
}
log("updateLocation(): Updated location with trigger " + trigger);
addToQueue(trigger);
}
/**
* Updates wifi scan results if collection is enabled
*
* @param currentScanResults scan results
*/
public synchronized void updateWifiScanResults(List<ScanResult> currentScanResults) {
if (!isCollectionEnabled()) {
return;
}
if (currentScanResults == null || currentScanResults.size() == 0) {
return;
}
long now = SystemClock.elapsedRealtime();
// If wifi scan recently received, then don't upload
if ((mLastWifiScanElapsedTime != 0)
&& ((now - mLastWifiScanElapsedTime) <= MIN_TIME_BETWEEN_WIFI_REPORTS)) {
return;
}
if (mWifiCurrentScanResults == null) {
mWifiCurrentScanResults = new ArrayList<ScanResult>();
} else {
mWifiCurrentScanResults.clear();
}
mWifiCurrentScanResults.addAll(currentScanResults);
// If wifi has changed enough
boolean wifiHasChanged = false;
if (mWifiLastScanResults == null) {
wifiHasChanged = true;
} else {
// Calculate the number of new AP points received
HashSet<String> previous = new HashSet<String>();
HashSet<String> current = new HashSet<String>();
for (ScanResult s : mWifiLastScanResults) {
previous.add(s.BSSID);
}
for (ScanResult s : mWifiCurrentScanResults) {
current.add(s.BSSID);
}
current.removeAll(previous);
if (current.size() >
Math.min(MIN_CHANGED_WIFI_POINTS, ((mWifiCurrentScanResults.size()+1)/2))) {
wifiHasChanged = true;
}
}
if (!wifiHasChanged) {
log("updateWifiScanResults(): Wifi results haven't changed much");
return;
}
if (mWifiLastScanResults == null) {
mWifiLastScanResults = new ArrayList<ScanResult>();
} else {
mWifiLastScanResults.clear();
}
mWifiLastScanResults.addAll(mWifiCurrentScanResults);
mLastWifiScanElapsedTime = now;
mLastWifiScanRealTime = System.currentTimeMillis();
log("updateWifiScanResults(): Updated " + mWifiLastScanResults.size() + " APs");
addToQueue(GDebugProfile.TRIGGER_WIFI_CHANGE);
}
/**
* Updates the status of the network location provider.
*
* @param enabled true if user has enabled network location based on Google's database
* of wifi points and cell towers.
*/
public void updateNetworkProviderStatus(boolean enabled) {
mNetworkProviderIsEnabled = enabled;
}
/**
* Updates the battery health. Battery level is healthy if there is greater than
* {@link #MIN_BATTERY_LEVEL} percentage left or if the device is plugged in
*
* @param scale maximum scale for battery
* @param level current level
* @param plugged true if device is plugged in
*/
public void updateBatteryState(int scale, int level, boolean plugged) {
mBatteryLevelIsHealthy = (plugged || (level >= (MIN_BATTERY_LEVEL * scale)));
mBatteryChargedAndPlugged = (plugged && (level >= (CHARGED_BATTERY_LEVEL * scale)));
}
/**
* Anonymous data collection is only enabled when the user has enabled the network
* location provider, i.e. is making use of the service and if the device battery level
* is healthy.
*
* Additionally, data collection will *never* happen if the system
* property ro.com.google.locationfeatures is not set.
*
* @return true if anonymous location collection is enabled
*/
private boolean isCollectionEnabled() {
// This class provides a Google-specific location feature, so it's enabled only
// when the system property ro.com.google.locationfeatures is set.
if (!SystemProperties.get("ro.com.google.locationfeatures").equals("1")) {
return false;
}
return mBatteryLevelIsHealthy && mNetworkProviderIsEnabled;
}
/**
* Adds to the MASF request queue
*
* @param trigger the event that triggered this collection event
*/
private synchronized void addToQueue(int trigger) {
long now = SystemClock.elapsedRealtime();
// Include location if:
// It has been received in the last 12 minutes.
boolean includeLocation = false;
if (mLastKnownLocation != null &&
(now - mLastKnownLocationTime <= MIN_VALID_LOCATION_TIME)) {
includeLocation = true;
}
// Include wifi if:
// Wifi is new OR
// Wifi is old but last wifi upload was without location
boolean includeWifi = false;
if (trigger == GDebugProfile.TRIGGER_WIFI_CHANGE || (mWifiUploadedWithoutLocation &&
includeLocation && (now - mLastWifiScanElapsedTime < MIN_VALID_LOCATION_TIME))) {
includeWifi = true;
mWifiUploadedWithoutLocation = !includeLocation;
}
// Include cell if:
// Wifi or location information is already being included
// The cell hasn't been uploaded with the same location recently
boolean includeCell = false;
if (mCellState != null && (includeWifi || includeLocation)) {
includeCell = true;
if (!includeWifi && includeLocation) {
if (mCellUploads.contains(mCellState, mLastKnownLocation)) {
includeCell = false;
}
}
}
if (!includeLocation && !includeWifi) {
log("NO UPLOAD: includeLocation=false, includeWifi=false");
return;
} else if (!includeCell && trigger == GDebugProfile.TRIGGER_CELL_CHANGE) {
log("NO UPLOAD: includeCell=false");
return;
} else {
log("UPLOAD: includeLocation=" + includeLocation + ", includeWifi=" +
includeWifi + ", includeCell=" + includeCell);
}
if (includeLocation) {
// Update last uploaded location
if (mLastUploadedLocation == null) {
mLastUploadedLocation = new Location(mLastKnownLocation);
} else {
mLastUploadedLocation.set(mLastKnownLocation);
}
mLastUploadedLocationTime = now;
}
// Immediately send output if finishing a burst for live traffic requirements
boolean immediate = false;
if (trigger == GDebugProfile.TRIGGER_COLLECTION_END_BURST||
trigger == GDebugProfile.TRIGGER_COLLECTION_END_BURST_AT_SAME_LOCATION) {
immediate = true;
}
try {
CellState cell = includeCell ? mCellState : null;
List<ScanResult> wifi = includeWifi ? mWifiLastScanResults : null;
Location loc = includeLocation ? mLastUploadedLocation : null;
mMasfClient.queueCollectionReport(
trigger, loc, cell, wifi, mLastWifiScanRealTime, immediate);
} catch(Exception e) {
Log.e(TAG, "addToQueue got exception:", e);
}
}
private class CellUploads {
private final int MIN_DISTANCE = MIN_DISTANCE_BETWEEN_REPORTS / 4; // 50 meters
private final int SIZE = 5;
private final String[] cells = new String[SIZE];
private final boolean[] valid = new boolean[SIZE];
private final double[] latitudes = new double[SIZE];
private final double[] longitudes = new double[SIZE];
private final float[] distance = new float[1];
private int index = 0;
private CellUploads() {
for (int i = 0; i < SIZE; i++) {
valid[i] = false;
}
}
private boolean contains(CellState cellState, Location loc) {
String cell =
cellState.getCid() + ":" + cellState.getLac() + ":" +
cellState.getMnc() + ":" + cellState.getMcc();
double lat = loc.getLatitude();
double lng = loc.getLongitude();
for (int i = 0; i < SIZE; i++) {
if (valid[i] && cells[i].equals(cell)) {
Location.distanceBetween(latitudes[i], longitudes[i], lat, lng, distance);
if (distance[0] < MIN_DISTANCE) {
return true;
}
}
}
cells[index] = cell;
latitudes[index] = lat;
longitudes[index] = lng;
valid[index] = true;
index++;
if (index == SIZE) {
index = 0;
}
return false;
}
}
private void log(String string) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.d(TAG, string);
}
}
}