summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/java/android/provider/Settings.java5
-rw-r--r--location/java/android/location/ILocationManager.aidl3
-rw-r--r--location/java/android/location/LocationManager.java8
-rw-r--r--services/java/com/android/server/LocationManagerService.java50
-rw-r--r--services/java/com/android/server/location/BTGPSService.java465
-rw-r--r--services/java/com/android/server/location/BTGpsLocationProvider.java858
-rw-r--r--services/java/com/android/server/location/NMEAParser.java544
7 files changed, 1925 insertions, 8 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 210379d..61fd272 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2965,6 +2965,11 @@ public final class Settings {
public static final String ASSISTED_GPS_ENABLED = "assisted_gps_enabled";
/**
+ * External GPS source/device
+ * @hide
+ */
+ public static final String EXTERNAL_GPS_BT_DEVICE = "0";
+ /**
* The Logging ID (a unique 64-bit value) as a hex string.
* Used as a pseudonymous identifier for logging.
* @deprecated This identifier is poorly initialized and has
diff --git a/location/java/android/location/ILocationManager.aidl b/location/java/android/location/ILocationManager.aidl
index 2255bf2..3e7437b 100644
--- a/location/java/android/location/ILocationManager.aidl
+++ b/location/java/android/location/ILocationManager.aidl
@@ -88,4 +88,7 @@ interface ILocationManager
// for NI support
boolean sendNiResponse(int notifId, int userResponse);
+
+ // add set gps source
+ void setGPSSource(String device);
}
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index 2817df8..9a66039 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -272,6 +272,14 @@ public class LocationManager {
return provider;
}
+ public void setGPSSource(String device) {
+ try {
+ mService.setGPSSource(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+
/**
* Returns a list of the names of all known location providers. All
* providers are returned, including ones that are not permitted to be
diff --git a/services/java/com/android/server/LocationManagerService.java b/services/java/com/android/server/LocationManagerService.java
index 56afe7f..9d893aa 100644
--- a/services/java/com/android/server/LocationManagerService.java
+++ b/services/java/com/android/server/LocationManagerService.java
@@ -54,6 +54,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.WorkSource;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.util.PrintWriterPrinter;
@@ -61,6 +62,7 @@ import android.util.PrintWriterPrinter;
import com.android.internal.content.PackageMonitor;
import com.android.internal.location.GpsNetInitiatedHandler;
+import com.android.server.location.BTGpsLocationProvider;
import com.android.server.location.GeocoderProxy;
import com.android.server.location.GpsLocationProvider;
import com.android.server.location.LocationProviderInterface;
@@ -466,16 +468,48 @@ public class LocationManagerService extends ILocationManager.Stub implements Run
}
}
+ public void setGPSSource(String device) {
+ synchronized (mLock) {
+ if (mGpsLocationProvider != null &&
+ mProvidersByName.containsKey(mGpsLocationProvider.getName())) {
+ Slog.i(TAG, "Disable and removing provider " + mGpsLocationProvider.getName());
+ mGpsLocationProvider.disable();
+ Settings.Secure.setLocationProviderEnabled(mContext.getContentResolver(),
+ LocationManager.GPS_PROVIDER, false);
+ removeProvider(mGpsLocationProvider);
+ mGpsLocationProvider = null;
+ }
+ Slog.i(TAG, "Setting GPS Source to: " + device);
+ if ("0".equals(device)) {
+ if (mGpsLocationProvider != null && !GpsLocationProvider.isSupported())
+ return;
+ GpsLocationProvider gpsProvider = new GpsLocationProvider(mContext, this);
+ mGpsStatusProvider = gpsProvider.getGpsStatusProvider();
+ mNetInitiatedListener = gpsProvider.getNetInitiatedListener();
+ addProvider(gpsProvider);
+ mGpsLocationProvider = gpsProvider;
+ } else {
+ BTGpsLocationProvider gpsProvider = new BTGpsLocationProvider(mContext, this);
+ mGpsStatusProvider = gpsProvider.getGpsStatusProvider();
+ mNetInitiatedListener = null;
+ addProvider(gpsProvider);
+ mGpsLocationProvider = gpsProvider;
+ }
+ }
+ }
+
private void _loadProvidersLocked() {
// Attempt to load "real" providers first
- if (GpsLocationProvider.isSupported()) {
- // Create a gps location provider
- GpsLocationProvider gpsProvider = new GpsLocationProvider(mContext, this);
- mGpsStatusProvider = gpsProvider.getGpsStatusProvider();
- mNetInitiatedListener = gpsProvider.getNetInitiatedListener();
- addProvider(gpsProvider);
- mGpsLocationProvider = gpsProvider;
- }
+ // Create a gps location provider based on the setting EXTERNAL_GPS_BT_DEVICE
+ String btDevice = Settings.System.getString(mContext.getContentResolver(),
+ Settings.Secure.EXTERNAL_GPS_BT_DEVICE);
+ if (TextUtils.isEmpty(btDevice)) {
+ // default option
+ btDevice = "0";
+ Settings.System.putString(mContext.getContentResolver(),
+ Settings.Secure.EXTERNAL_GPS_BT_DEVICE, btDevice);
+ }
+ setGPSSource(btDevice);
// create a passive location provider, which is always enabled
PassiveProvider passiveProvider = new PassiveProvider(this);
diff --git a/services/java/com/android/server/location/BTGPSService.java b/services/java/com/android/server/location/BTGPSService.java
new file mode 100644
index 0000000..49aa20d
--- /dev/null
+++ b/services/java/com/android/server/location/BTGPSService.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2011 Cuong Bui
+ *
+ * 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.server.location;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.UUID;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+public class BTGPSService {
+ private static final boolean D = true;
+ private static final String TAG = "BTGPSService";
+ private static final UUID BT_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
+ private final BluetoothAdapter mAdapter;
+ private final Handler mHandler;
+ private WatchdogThread mWatchdogThread = null;
+ private ConnectThread mConnectThread = null;
+ private ConnectedThread mConnectedThread = null;
+ private final int mMaxNMEABuffer=4096;
+ private final char[] buffer = new char[mMaxNMEABuffer];
+ int bytes;
+ private long refreshRate = 1000;
+ private long lastActivity = 0;
+ // MAX_ACTIVITY_TIMEOUT * refresh time window should have at least one activity.
+ private int MAX_ACTIVITY_TIMEOUT = 5;
+ // Maximum connect retry attempt
+ private int MAX_RECONNECT_RETRIES = 5;
+ // time window for one single connection (ms). socket connect timeout is around 12 sec
+ private int MAX_CONNECT_TIMEOUT = 13000;
+ // last connected device. is used to auto reconnect.
+ private BluetoothDevice lastConnectedDevice=null;
+
+ private int mState = 0;
+ // Constants that indicate the current connection state
+ public static final int STATE_NONE = 0; // we're doing nothing
+ public static final int STATE_LISTEN = 1; // now listening for incoming connections
+ public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
+ public static final int STATE_CONNECTED = 3; // now connected to a remote device
+
+ public synchronized void setRefreshRate(long r) {
+ refreshRate = r;
+ }
+
+ public synchronized long getRefreshRate() {
+ return refreshRate;
+ }
+
+ public BTGPSService(Handler h) {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mHandler = h;
+ }
+
+ private void sendMessage(int message, int arg, Object obj) {
+ Message m = Message.obtain(mHandler, message);
+ m.arg1 = arg;
+ m.obj = obj;
+ mHandler.sendMessage(m);
+ }
+
+ private void handleFailedConnection() {
+ if (getServiceState() != STATE_NONE) {
+ if (D) Log.d(TAG, "Connection failed with status != 0. try to reconnect");
+ connect(lastConnectedDevice);
+ } else {
+ if (D) Log.d(TAG, "Connection stopped with status = 0.");
+ }
+ }
+
+ /**
+ * Set the current state of the chat connection
+ * @param state An integer defining the current connection state
+ */
+ private synchronized void setState(int state) {
+ if (D) Log.d(TAG, "setState() " + mState + " -> " + state);
+ mState = state;
+ if (mState == STATE_NONE) {
+ sendMessage(BTGpsLocationProvider.GPS_STATUS_UPDATE, 0, null);
+ } else if (mState == STATE_CONNECTED) {
+ sendMessage(BTGpsLocationProvider.GPS_STATUS_UPDATE, 1, null);
+ }
+ }
+
+ /**
+ * Return the current connection state. */
+ public synchronized int getServiceState() {
+ return mState;
+ }
+
+ /**
+ * Start the chat service. Specifically start AcceptThread to begin a
+ * session in listening (server) mode. Called by the Activity onResume() */
+ public synchronized void start() {
+
+ if (D) Log.d(TAG, "start");
+ if (!mAdapter.isEnabled()) {
+ setState(STATE_NONE);
+ return;
+ }
+ // Cancel any thread attempting to make a connection
+ if (mConnectThread != null) {
+ mConnectThread.cancel();
+ mConnectThread = null;
+ }
+ // Cancel any thread currently running a connection
+ if (mConnectedThread != null) {
+ mConnectedThread.cancel();
+ mConnectedThread = null;
+ }
+ setState(STATE_LISTEN);
+ }
+
+ /**
+ * Start the ConnectThread to initiate a connection to a remote device.
+ * @param device The BluetoothDevice to connect
+ */
+ public synchronized boolean connect(BluetoothDevice device) {
+ lastConnectedDevice = device;
+ if (D) Log.d(TAG, "connect to: " + device);
+ // Cancel any thread attempting to make a connection
+ if (mConnectThread != null) {
+ mConnectThread.cancel();
+ mConnectThread = null;
+ }
+ if (mWatchdogThread != null) {
+ mWatchdogThread.cancel();
+ mWatchdogThread = null;
+ }
+ // Cancel any thread currently running a connection
+ if (mConnectedThread != null) {
+ mConnectedThread.cancel();
+ mConnectedThread = null;
+ }
+ // Helper thread that monitors and retries to connect after time out
+ mWatchdogThread = new WatchdogThread(device);
+ mWatchdogThread.start();
+ return true;
+ }
+
+ /**
+ * Start the ConnectedThread to begin managing a Bluetooth connection
+ * @param socket The BluetoothSocket on which the connection was made
+ * @param device The BluetoothDevice that has been connected
+ */
+ public synchronized void connected(BluetoothSocket socket) {
+ // reset connect thread
+ if (mConnectThread != null) mConnectThread = null;
+
+ // kill watchdog, since we are connected
+ if (mWatchdogThread != null) {
+ mWatchdogThread.cancel();
+ mWatchdogThread = null;
+ }
+ // Cancel any thread currently running a connection
+ if (mConnectedThread != null) {
+ mConnectedThread.cancel();
+ mConnectedThread = null;
+ }
+
+ // Start the thread to manage the connection and perform transmissions
+ mConnectedThread = new ConnectedThread(socket);
+ mConnectedThread.start();
+ setState(STATE_CONNECTED);
+ }
+
+ /**
+ * Stop all threads
+ */
+ public synchronized void stop() {
+ if (D) Log.d(TAG, "Stopping btsvc, Set state to None");
+ setState(STATE_NONE);
+
+ if (mWatchdogThread != null) {
+ if (D) Log.d(TAG, "Cancelling watchdog thread");
+ mWatchdogThread.cancel();
+ mWatchdogThread = null;
+ }
+
+ if (mConnectThread != null) {
+ if (D) Log.d(TAG, "Cancelling connect thread");
+ mConnectThread.cancel();
+ mConnectThread = null;
+ }
+ if (mConnectedThread != null) {
+ if (D) Log.d(TAG, "Cancelling connected thread");
+ mConnectedThread.cancel();
+ mConnectedThread = null;
+ }
+ }
+
+ /**
+ * Write to the ConnectedThread in an unsynchronized manner
+ * @param out The bytes to write
+ * @see ConnectedThread#write(byte[])
+ */
+ public void write(byte[] out) {
+ // Create temporary object
+ ConnectedThread r;
+ // Synchronize a copy of the ConnectedThread
+ synchronized (this) {
+ if (mState != STATE_CONNECTED) return;
+ r = mConnectedThread;
+ }
+ r.write(out);
+ }
+
+ /**
+ * This thread runs while attempting to make an outgoing connection
+ * with a device. It runs straight through; the connection either
+ * succeeds or fails.
+ */
+ private class ConnectThread extends Thread {
+ private BluetoothSocket mmSocket;
+ private final BluetoothDevice mmDevice;
+ private String mSocketType;
+
+ public ConnectThread(BluetoothDevice device) {
+ mmDevice = device;
+ }
+
+ private void closeSocket() {
+ if (D) Log.d(TAG, getId()+":close socket");
+ if (mmSocket == null) {
+ Log.e(TAG, getId()+":Socket not ready. Aborting Close");
+ return;
+ }
+
+ try {
+ mmSocket.close();
+ mmSocket = null;
+ } catch (IOException e) {
+ Log.e(TAG, getId()+":close() of connect " + mSocketType + " socket failed", e);
+ }
+ }
+
+ public void run() {
+ Log.i(TAG, getId() + ":begin mConnectThread");
+ BluetoothSocket tmp = null;
+ // Always cancel discovery because it will slow down a connection
+
+ try {
+ tmp = mmDevice.createRfcommSocketToServiceRecord(BT_UUID);
+ } catch (IOException e) {
+ Log.e(TAG, "Socket create() failed", e);
+ return;
+ }
+ mmSocket = tmp;
+ // Make a connection to the BluetoothSocket
+ if (mAdapter.isEnabled()) mAdapter.cancelDiscovery();
+ try {
+ // This is a blocking call and will only return on a
+ // successful connection or an exception
+ if (D) Log.d(TAG, getId() + ":Connecting to socket...");
+ mmSocket.connect();
+ if (D) Log.d(TAG, "connected with remote device: "
+ + mmDevice.getName() + " at address " + mmDevice.getAddress());
+ connected(mmSocket);
+ } catch (IOException e) {
+ Log.w(TAG, getId() + ":connect failed.", e);
+ return;
+ }
+ }
+
+ public synchronized void cancel() {
+ closeSocket();
+ }
+ }
+
+ /**
+ * This thread runs during a connection with a remote device.
+ * It handles all incoming and outgoing transmissions.
+ */
+ private class ConnectedThread extends Thread {
+ private BluetoothSocket mmSocket;
+ private InputStream mmInStream;
+ private OutputStream mmOutStream;
+ private boolean cancelled = false;
+
+ private void closeSocket() {
+ if (D) Log.d(TAG, getId()+":close socket");
+ if (mmSocket == null) {
+ Log.e(TAG, getId()+":Socket not ready. Aborting Close");
+ return;
+ }
+ try {
+ mmSocket.close();
+ mmSocket = null;
+ } catch (IOException e) {
+ Log.e(TAG, getId()+": close() of connect socket failed", e);
+ }
+ }
+
+ public ConnectedThread(BluetoothSocket socket) {
+ Log.d(TAG, getId() + ":begin ConnectedThread");
+ mmSocket = socket;
+ InputStream tmpIn = null;
+ OutputStream tmpOut = null;
+
+ // Get the BluetoothSocket input and output streams
+ try {
+ tmpIn = socket.getInputStream();
+ tmpOut = socket.getOutputStream();
+ } catch (IOException e) {
+ Log.e(TAG, "temp sockets not created", e);
+ }
+ mmInStream = tmpIn;
+ mmOutStream = tmpOut;
+ }
+
+ public void run() {
+ if (mmSocket == null || mmInStream == null) {
+ Log.e(TAG, "Input stream or socket is null. Aborting thread");
+ return;
+ }
+ if (D) Log.d(TAG, getId() + ":BEGIN mConnectedThread");
+ java.util.Arrays.fill(buffer, (char) ' ');
+ // reset refresh rate to 1000
+ refreshRate = 1000;
+ lastActivity = 0;
+ BufferedReader reader = new BufferedReader(new InputStreamReader(mmInStream));
+ // Keep listening to the InputStream while connected
+ while (true) {
+ try {
+ if (reader.ready()) {
+ bytes = reader.read(buffer, 0, mMaxNMEABuffer);
+ Message msg = mHandler.obtainMessage(
+ BTGpsLocationProvider.GPS_DATA_AVAILABLE,buffer);
+ lastActivity = System.currentTimeMillis();
+ msg.arg1 = bytes;
+ mHandler.sendMessage(msg);
+ }
+ if (lastActivity != 0 && (System.currentTimeMillis() - lastActivity) >
+ MAX_ACTIVITY_TIMEOUT*refreshRate) {
+ Log.w(TAG, getId() + ":BT activity timeout.");
+ closeSocket();
+ handleFailedConnection();
+ return;
+ }
+ try {
+ // get default sleep time
+ Thread.sleep(getRefreshRate());
+ } catch (InterruptedException e) {
+ if (cancelled) {
+ closeSocket();
+ return;
+ }
+ }
+ } catch (IOException e) {
+ Log.w(TAG, getId() + ":disconnected.", e);
+ closeSocket();
+ handleFailedConnection();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Write to the connected OutStream.
+ * @param buffer The bytes to write
+ */
+ public void write(byte[] buffer) {
+ try {
+ mmOutStream.write(buffer);
+ mmOutStream.flush();
+ } catch (IOException e) {
+ Log.e(TAG, "Exception during write", e);
+ }
+ }
+
+ public void cancel() {
+ try {
+ if (mmSocket == null) {
+ Log.e(TAG, "Input stream null. Aborting Cacnel");
+ return;
+ }
+ mmSocket.close();
+ } catch (IOException e) {
+ Log.e(TAG, "close() of connect socket failed", e);
+ } finally {
+ cancelled = true;
+ interrupt();
+ }
+ }
+ }
+ /*
+ * Thread that starts the connection thread an monitors it.
+ * Thread will be cancelled if timeot occurs
+ */
+ private class WatchdogThread extends Thread {
+ private final BluetoothDevice btdevice;
+ private int retries = 0;
+ private boolean sleep = false;
+ private boolean cancelled = false;
+
+ public WatchdogThread(BluetoothDevice dev) {
+ btdevice = dev;
+ }
+
+ public void run() {
+ while(retries < MAX_RECONNECT_RETRIES) {
+ if (mConnectThread != null) {
+ mConnectThread.cancel();
+ mConnectThread = null;
+ }
+ if (mConnectedThread != null) {
+ mConnectedThread.cancel();
+ mConnectedThread = null;
+ }
+
+ mConnectThread = new ConnectThread(btdevice);
+ mConnectThread.start();
+ setState(STATE_CONNECTING);
+ // monitor connection and cancel if timeout
+ if (D) Log.d(TAG, getId() + ":Waiting " + MAX_CONNECT_TIMEOUT
+ + " (ms) for service to connect...");
+ try {
+ sleep = true;
+ Thread.sleep(MAX_CONNECT_TIMEOUT);
+ sleep = false;
+ if (D) Log.d(TAG, getId() + ":Connecting timeout.");
+ } catch (InterruptedException e) {
+ if (D) Log.d(TAG, getId() + ":Watchdog interrupted. probably by cancel.");
+ }
+ if (getServiceState() == STATE_CONNECTED) {
+ if (D) Log.d(TAG, getId() + ":Connected. aborting watchdog");
+ return;
+ }
+ if (cancelled) {
+ if (D) Log.d(TAG, getId() + ":Cancelled. aborting watchdog");
+ return;
+ }
+ retries++;
+ }
+ // max timeout, so stopping service
+ if (D) Log.d(TAG, getId() + ":Max connection retries exceeded. stopping services.");
+ BTGPSService.this.stop();
+ }
+
+ public void cancel() {
+ cancelled = true;
+ if (sleep) interrupt();
+ }
+ }
+}
diff --git a/services/java/com/android/server/location/BTGpsLocationProvider.java b/services/java/com/android/server/location/BTGpsLocationProvider.java
new file mode 100644
index 0000000..8f2bbab
--- /dev/null
+++ b/services/java/com/android/server/location/BTGpsLocationProvider.java
@@ -0,0 +1,858 @@
+/*
+ * Copyright (C) 2011 Cuong Bui
+ *
+ * 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.server.location;
+
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.Criteria;
+import android.location.IGpsStatusListener;
+import android.location.IGpsStatusProvider;
+import android.location.ILocationManager;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.WorkSource;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.internal.app.IBatteryStats;
+
+
+public class BTGpsLocationProvider implements LocationProviderInterface {
+ private static final boolean D = true;
+ private final String PROVIDER = "External Bleutooth Location Provider";
+ private final String TAG = "BTGpsLocationProvider";
+ private final NMEAParser nmeaparser = new NMEAParser(PROVIDER);
+
+ private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ // GPS update codes
+ public static final int GPS_DATA_AVAILABLE = 1000;
+ public static final int GPS_STATUS_UPDATE = 1001;
+ public static final int GPS_CUSTOM_COMMAND = 1002;
+
+
+ // Wakelocks
+ private final static String WAKELOCK_KEY = "GpsLocationProvider";
+ private final PowerManager.WakeLock mWakeLock;
+ // bitfield of pending messages to our Handler
+ // used only for messages that cannot have multiple instances queued
+ private int mPendingMessageBits;
+ // separate counter for ADD_LISTENER and REMOVE_LISTENER messages,
+ // which might have multiple instances queued
+ private int mPendingListenerMessages;
+
+ private final IBatteryStats mBatteryStats;
+ private final SparseIntArray mClientUids = new SparseIntArray();
+ // Handler messages
+ private static final int CHECK_LOCATION = 1;
+ private static final int ENABLE = 2;
+ private static final int ENABLE_TRACKING = 3;
+ private static final int UPDATE_NETWORK_STATE = 4;
+ private static final int INJECT_NTP_TIME = 5;
+ private static final int DOWNLOAD_XTRA_DATA = 6;
+ private static final int UPDATE_LOCATION = 7;
+ private static final int ADD_LISTENER = 8;
+ private static final int REMOVE_LISTENER = 9;
+ private static final int REQUEST_SINGLE_SHOT = 10;
+
+ // for calculating time to first fix
+ private long mFixRequestTime = 0;
+ // time to first fix for most recent session
+ private int mTTFF = 0;
+ // time we received our last fix
+ private long mLastFixTime;
+
+ // time for last status update
+ private long mStatusUpdateTime = SystemClock.elapsedRealtime();
+
+ // true if we are enabled
+ private volatile boolean mEnabled;
+
+ // true if GPS is navigating
+ private boolean mNavigating;
+
+ private int mSvCount;
+ // current status
+ private int mStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
+
+ private Bundle mLocationExtras = new Bundle();
+ private Location mLocation = new Location(PROVIDER);
+
+ private final Context mContext;
+ private final ILocationManager mLocationManager;
+
+ private final IntentFilter mIntentBTFilter;
+
+ private final Thread mMessageLoopThread = new BTGPSMessageThread();
+ private final CountDownLatch mInitializedLatch = new CountDownLatch(1);
+
+ /**
+ *Listen for BT changes. If BT is turned off, disable GPS services
+ */
+ private final BroadcastReceiver mReceiver;
+
+ /**
+ * Message handler
+ * Receives nmea sentences
+ * receives connection lost signals
+ * enabling/disabling gps signals
+ * adding/removing listeners
+ */
+ private Handler mHandler;
+
+ // BT gps service. This class handles the actual BT connection and data xfer
+ private final BTGPSService btsvc;
+
+ // BT Location provider , uses the same method signature as the org GPS location provider
+ public BTGpsLocationProvider(Context context, ILocationManager locationManager) {
+
+ mContext = context;
+ mLocationManager = locationManager;
+ // innit message handler
+ mMessageLoopThread.start();
+ // wait for message handler to be ready
+ while (true) {
+ try {
+ mInitializedLatch.await();
+ break;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ // instantiate BTGPSService
+ btsvc = new BTGPSService(mHandler);
+
+ // Create a wake lock.
+ PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
+ mWakeLock.setReferenceCounted(false);
+
+ // Battery statistics service to be notified when GPS turns on or off
+ mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batteryinfo"));
+
+ // receive BT state changes
+ mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ BluetoothAdapter.ERROR);
+ switch (state) {
+ case BluetoothAdapter.STATE_ON:
+ if (D) Log.i(TAG, "BT turned on -> notify services?");
+ break;
+ case BluetoothAdapter.STATE_TURNING_OFF:
+ if (btsvc.getServiceState() != BTGPSService.STATE_NONE) {
+ if (D) Log.i(TAG, "BT turned off -> stopping services");
+ btsvc.stop();
+ }
+ break;
+ }
+ }
+ }
+ };
+ mIntentBTFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiver(mReceiver, mIntentBTFilter);
+ }
+
+ private final class BTGPSMessageThread extends Thread {
+
+ public void run() {
+ try {
+ Looper.prepare();
+ } catch (RuntimeException e) {
+ // ignored: Looper already prepared
+ }
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ int message = msg.what;
+ switch (message) {
+ case GPS_DATA_AVAILABLE:
+ char[] writeBuf = (char[]) msg.obj;
+ int bytes = msg.arg1;
+ if ((writeBuf != null) && (mEnabled && bytes > 0)) {
+ String writeMessage = new String(writeBuf, 0, bytes);
+ handleNMEAMessages(writeMessage);
+ java.util.Arrays.fill(writeBuf, (char) ' ');
+ }
+ break;
+ case GPS_STATUS_UPDATE:
+ notifyEnableDisableGPS(msg.arg1 == 1);
+ break;
+ case GPS_CUSTOM_COMMAND:
+ if (mEnabled && btsvc.getServiceState() == BTGPSService.STATE_CONNECTED) {
+ // sends custom commands
+ byte[] cmds = (byte[]) msg.obj;
+ btsvc.write(cmds);
+ }
+ break;
+ case ENABLE:
+ if (msg.arg1 == 1) {
+ handleEnable();
+ } else {
+ handleDisable();
+ }
+ break;
+ case REQUEST_SINGLE_SHOT:
+ case ENABLE_TRACKING:
+ case UPDATE_NETWORK_STATE:
+ case INJECT_NTP_TIME:
+ case DOWNLOAD_XTRA_DATA:
+ break;
+ case UPDATE_LOCATION:
+ handleUpdateLocation((Location)msg.obj);
+ break;
+ case ADD_LISTENER:
+ handleAddListener(msg.arg1);
+ break;
+ case REMOVE_LISTENER:
+ handleRemoveListener(msg.arg1);
+ break;
+ }
+ // release wake lock if no messages are pending
+ synchronized (mWakeLock) {
+ mPendingMessageBits &= ~(1 << message);
+ if (message == ADD_LISTENER || message == REMOVE_LISTENER) {
+ mPendingListenerMessages--;
+ }
+ if (mPendingMessageBits == 0 && mPendingListenerMessages == 0) {
+ mWakeLock.release();
+ }
+ }
+ }
+ };
+ mInitializedLatch.countDown();
+ Looper.loop();
+ }
+ }
+
+ @Override
+ public void enable() {
+ synchronized (mHandler) {
+ sendMessage(ENABLE, 1, null);
+ }
+ }
+
+ /**
+ * Enables BT GPS provider
+ */
+ private synchronized void handleEnable() {
+ if (D) Log.d(TAG, "handleEnable");
+ if (mEnabled) return;
+ // check if BT is enabled
+ if (!mAdapter.isEnabled()) {
+ int state = mAdapter.getState();
+ if (state == BluetoothAdapter.STATE_OFF) {
+ if (D) Log.d(TAG, "BT not available. Enable and wait for it...");
+ mAdapter.enable();
+ }
+ // wait for adapter to be ready
+ while (true) {
+ try {
+ state = mAdapter.getState();
+ if (state == BluetoothAdapter.STATE_ON) {
+ break;
+ } else if (state == BluetoothAdapter.STATE_TURNING_ON) {
+ if (D) Log.d(TAG, "BT not available yet. waiting for another 400ms");
+ Thread.sleep(400);
+ } else {
+ if (D) Log.d(TAG, "BT got disabled or interrupted by other source");
+ return;
+ }
+
+ } catch (InterruptedException e) {
+ Log.w(TAG, e.getMessage());
+ }
+ }
+ }
+ if (D) Log.d(TAG, "mEnabled -> true");
+ mEnabled = true;
+ if (D) Log.d(TAG, "mStatus -> temp unavailable");
+ mStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
+ if (D) Log.d(TAG, "btservice start");
+ btsvc.start();
+ mFixRequestTime = System.currentTimeMillis();
+ mTTFF = 0;
+ String btDevice = Settings.System.getString(mContext.getContentResolver(),
+ Settings.Secure.EXTERNAL_GPS_BT_DEVICE);
+ if (D) Log.d(TAG, "Connecting to saved pref: " + btDevice);
+ if ((btDevice != null) && !"0".equals(btDevice)) {
+ if ((mAdapter != null) && (mAdapter.isEnabled())) {
+ for (BluetoothDevice d: mAdapter.getBondedDevices()) {
+ if (btDevice.equals(d.getAddress())) {
+ if (D) Log.d(TAG, "Connecting...");
+ btsvc.connect(d);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Disables this provider.
+ */
+ @Override
+ public void disable() {
+ synchronized (mHandler) {
+ sendMessage(ENABLE, 0, null);
+ }
+ }
+
+ private synchronized void handleDisable() {
+ if (D) Log.d(TAG, "handleDisable");
+ if (!mEnabled) return;
+ if (D) Log.d(TAG, "mEnabled -> false");
+ mEnabled = false;
+ if (D) Log.d(TAG, "reportstatus notify listeners and system");
+ notifyEnableDisableGPS(false);
+ if (D) Log.d(TAG, "update to out of service");
+ updateStatus(LocationProvider.OUT_OF_SERVICE, mSvCount);
+ if (D) Log.d(TAG, "btservice Stop");
+ btsvc.stop();
+ }
+
+ /* We do not need to implement scheduled tracking. With internal gps providers it makes sence
+ * to hibernate and resume periodically. With BT GPS providers it doesn't make sense
+ * @see com.android.server.location.LocationProviderInterface#enableLocationTracking(boolean)
+ */
+ @Override
+ public void enableLocationTracking(boolean enable) {
+ }
+
+ @Override
+ public int getAccuracy() {
+ return Criteria.ACCURACY_FINE;
+ }
+
+ /* Debug native state used by normal GPS provider only
+ * @see com.android.server.location.LocationProviderInterface#getInternalState()
+ */
+ @Override
+ public String getInternalState() {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return LocationManager.GPS_PROVIDER;
+ }
+
+ /**
+ * Returns the power requirement for this provider.
+ *
+ * @return the power requirement for this provider, as one of the
+ * constants Criteria.POWER_REQUIREMENT_*.
+ */
+ public int getPowerRequirement() {
+ return Criteria.POWER_MEDIUM;
+ }
+
+ /**
+ * Returns true if this provider meets the given criteria,
+ * false otherwise.
+ */
+ public boolean meetsCriteria(Criteria criteria) {
+ return (criteria.getPowerRequirement() != Criteria.POWER_LOW);
+ }
+
+ @Override
+ public int getStatus(Bundle extras) {
+ if (extras != null) {
+ extras.putInt("satellites", mSvCount);
+ }
+ return mStatus;
+ }
+
+ @Override
+ public long getStatusUpdateTime() {
+ return mStatusUpdateTime;
+ }
+
+ @Override
+ public boolean hasMonetaryCost() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+
+ @Override
+ public boolean requestSingleShotFix() {
+ return false;
+ }
+
+ @Override
+ public boolean requiresCell() {
+ return false;
+ }
+
+ @Override
+ public boolean requiresNetwork() {
+ return false;
+ }
+
+ @Override
+ public boolean requiresSatellite() {
+ return true;
+ }
+
+ @Override
+ public boolean sendExtraCommand(String command, Bundle extras) {
+ if (TextUtils.isEmpty(command)) return false;
+ synchronized (mHandler) {
+ String customCommand = command + "\r\n";
+ sendMessage(GPS_CUSTOM_COMMAND, customCommand.length(), customCommand.getBytes());
+ }
+ return true;
+ }
+
+ /* GPS scheduling stuff, not needed
+ * @see com.android.server.location.LocationProviderInterface#setMinTime(long, android.os.WorkSource)
+ */
+ @Override
+ public void setMinTime(long minTime, WorkSource ws) {
+
+ }
+
+ @Override
+ public boolean supportsAltitude() {
+ return mLocation.hasAltitude();
+ }
+
+ @Override
+ public boolean supportsBearing() {
+ return mLocation.hasBearing();
+ }
+
+ @Override
+ public boolean supportsSpeed() {
+ return mLocation.hasSpeed();
+ }
+
+ @Override
+ /**
+ * This is called to inform us when another location provider returns a location.
+ * Someday we might use this for network location injection to aid the GPS
+ */
+ public void updateLocation(Location location) {
+ sendMessage(UPDATE_LOCATION, 0, location);
+ }
+
+ private void handleUpdateLocation(Location location) {
+ if (location.hasAccuracy()) {
+ // Allow other provider GPS data ? discard for now
+ }
+ }
+
+ /* unneeded by BT GPS provider
+ * @see com.android.server.location.LocationProviderInterface#updateNetworkState(int, android.net.NetworkInfo)
+ */
+ @Override
+ public void updateNetworkState(int state, NetworkInfo info) {
+ // TODO Auto-generated method stub
+
+ }
+
+ /**
+ * @param loc Location object representing the fix
+ * @param isValid true if fix was valid
+ */
+ private void reportLocation(Location loc, boolean isValid) {
+
+ if (!isValid) {
+ if (mStatus == LocationProvider.AVAILABLE && mTTFF > 0) {
+ if (D) Log.d(TAG, "Invalid sat fix -> sending notification to system");
+ // send an intent to notify that the GPS is no longer receiving fixes.
+ Intent intent = new Intent(LocationManager.GPS_FIX_CHANGE_ACTION);
+ intent.putExtra(LocationManager.EXTRA_GPS_ENABLED, false);
+ mContext.sendBroadcast(intent);
+ updateStatus(LocationProvider.TEMPORARILY_UNAVAILABLE, mSvCount);
+ }
+ return;
+ }
+
+ synchronized (mLocation) {
+ mLocation.set(loc);
+ mLocation.setProvider(this.getName());
+ if (D) {
+ Log.d(TAG, "reportLocation lat: " + mLocation.getLatitude() +
+ " long: " + mLocation.getLongitude() + " alt: " + mLocation.getAltitude() +
+ " accuracy: " + mLocation.getAccuracy() + " timestamp: " + mLocation.getTime());
+ }
+ try {
+ mLocationManager.reportLocation(mLocation, false);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException calling reportLocation");
+ }
+ }
+
+ mLastFixTime = System.currentTimeMillis();
+ // report time to first fix
+ if ((mTTFF == 0) && (isValid)) {
+ mTTFF = (int)(mLastFixTime - mFixRequestTime);
+ if (D) Log.d(TAG, "TTFF: " + mTTFF);
+
+ // notify status listeners
+ synchronized(mListeners) {
+ int size = mListeners.size();
+ for (int i = 0; i < size; i++) {
+ Listener listener = mListeners.get(i);
+ try {
+ listener.mListener.onFirstFix(mTTFF);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException in first fix notification");
+ mListeners.remove(listener);
+ // adjust for size of list changing
+ size--;
+ }
+ }
+ }
+ }
+
+ if (mStatus != LocationProvider.AVAILABLE) {
+ if (D) Log.d(TAG,"Notify that we're receiving fixes");
+ // send an intent to notify that the GPS is receiving fixes.
+ Intent intent = new Intent(LocationManager.GPS_FIX_CHANGE_ACTION);
+ intent.putExtra(LocationManager.EXTRA_GPS_ENABLED, true);
+ mContext.sendBroadcast(intent);
+ updateStatus(LocationProvider.AVAILABLE, mSvCount);
+ }
+
+ }
+
+ /* report sats status
+ */
+ private void reportSvStatus(int svCount, int mSvs[], float mSnrs[],
+ float mSvElevations[], float mSvAzimuths[], int mSvMasks[]) {
+
+ if (D) Log.d(TAG,"About to report sat status svcount: " + svCount);
+ synchronized(mListeners) {
+ int size = mListeners.size();
+ for (int i = 0; i < size; i++) {
+ Listener listener = mListeners.get(i);
+ try {
+ listener.mListener.onSvStatusChanged(svCount, mSvs, mSnrs, mSvElevations,
+ mSvAzimuths, mSvMasks[NMEAParser.EPHEMERIS_MASK],
+ mSvMasks[NMEAParser.ALMANAC_MASK],
+ mSvMasks[NMEAParser.USED_FOR_FIX_MASK]);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException in reportSvInfo");
+ mListeners.remove(listener);
+ // adjust for size of list changing
+ size--;
+ }
+ }
+ }
+
+ // return number of sets used in fix instead of total
+ updateStatus(mStatus, Integer.bitCount(mSvMasks[NMEAParser.USED_FOR_FIX_MASK]));
+ }
+
+ /**
+ * Handles GPS status.
+ * will also inform listeners when GPS started/stopped
+ * @param status new GPS status
+ */
+ private void notifyEnableDisableGPS(boolean status) {
+ if (D) Log.v(TAG, "notifyEnableDisableGPS status: " + status);
+
+ synchronized(mListeners) {
+ mNavigating = status;
+ int size = mListeners.size();
+ for (int i = 0; i < size; i++) {
+ Listener listener = mListeners.get(i);
+ try {
+ if (status) {
+ listener.mListener.onGpsStarted();
+ } else {
+ listener.mListener.onGpsStopped();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException in reportStatus");
+ mListeners.remove(listener);
+ // adjust for size of list changing
+ size--;
+ }
+ }
+ try {
+ // update battery stats
+ for (int i=mClientUids.size() - 1; i >= 0; i--) {
+ int uid = mClientUids.keyAt(i);
+ if (mNavigating) {
+ mBatteryStats.noteStartGps(uid);
+ } else {
+ mBatteryStats.noteStopGps(uid);
+ }
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException in reportStatus");
+ }
+ // send an intent to notify that the GPS has been enabled or disabled.
+ Intent intent = new Intent(LocationManager.GPS_ENABLED_CHANGE_ACTION);
+ intent.putExtra(LocationManager.EXTRA_GPS_ENABLED, status);
+ mContext.sendBroadcast(intent);
+ }
+ try {
+ if (D) Log.d(TAG, "Setting System GPS status to " + status);
+ Settings.Secure.setLocationProviderEnabled(mContext.getContentResolver(),
+ LocationManager.GPS_PROVIDER, status);
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+
+ /**
+ * sends nmea sentences to NMEA parsers. Some apps use raw nmea data
+ * @param nmeaString nmea string
+ * @param timestamp time stamp
+ */
+ private void reportNmea(String nmeaString, long timestamp) {
+ synchronized(mListeners) {
+ int size = mListeners.size();
+ if (size > 0) {
+ // don't bother creating the String if we have no listeners
+ for (int i = 0; i < size; i++) {
+ Listener listener = mListeners.get(i);
+ try {
+ listener.mListener.onNmeaReceived(timestamp, nmeaString);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException in reportNmea");
+ mListeners.remove(listener);
+ // adjust for size of list changing
+ size--;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * This methods parses the nmea sentences and sends the location updates
+ * and sats updates to listeners.
+ * @param sentences raw nmea sentences received by BT GPS Mouse
+ */
+ private void handleNMEAMessages(String sentences) {
+ String sentenceArray[] = sentences.split("\r\n");
+ nmeaparser.reset();
+ for (int i = 0; i < sentenceArray.length; i++) {
+ if (D) Log.d(TAG, "About to parse: " + sentenceArray[i]);
+ if ((sentenceArray[i] != null) && ("".equals(sentenceArray[i]))) continue;
+ boolean parsed = nmeaparser.parseNMEALine(sentenceArray[i]);
+ // handle nmea message. Also report messages that we could not parse as these
+ // might be propriatery messages that other listeners could support.
+ reportNmea(sentenceArray[i], System.currentTimeMillis());
+ }
+ Location loc = nmeaparser.getLocation();
+ // handle location update if valid
+ reportLocation(loc , nmeaparser.isValid());
+ if (nmeaparser.isSatdataReady()) {
+ reportSvStatus(nmeaparser.getmSvCount(), nmeaparser.getmSvs(), nmeaparser.getmSnrs(),
+ nmeaparser.getmSvElevations(), nmeaparser.getmSvAzimuths(),
+ nmeaparser.getmSvMasks());
+ }
+
+ // adjust refresh rate based on received timestamp of mouse
+ // min 1hz and max 10 hz
+ long newRate = nmeaparser.getApproximatedRefreshRate();
+ if (btsvc.getRefreshRate() != newRate) {
+ if (D) Log.d(TAG, "Setting refresh rate to: " + newRate
+ + " was: " + btsvc.getRefreshRate());
+ btsvc.setRefreshRate(newRate);
+ }
+ }
+
+
+ /*
+ * Stuff below is taken from the android GPS location provider.
+ * Does handling of messages/listeners and so on.
+ */
+
+ private void sendMessage(int message, int arg, Object obj) {
+ // hold a wake lock while messages are pending
+ synchronized (mWakeLock) {
+ mPendingMessageBits |= (1 << message);
+ mWakeLock.acquire();
+ mHandler.removeMessages(message);
+ Message m = Message.obtain(mHandler, message);
+ m.arg1 = arg;
+ m.obj = obj;
+ mHandler.sendMessage(m);
+ }
+ }
+
+
+ private void updateStatus(int status, int svCount) {
+ if (status != mStatus || svCount != mSvCount) {
+ mStatus = status;
+ mSvCount = svCount;
+ mLocationExtras.putInt("satellites", svCount);
+ mStatusUpdateTime = SystemClock.elapsedRealtime();
+ }
+ }
+ private ArrayList<Listener> mListeners = new ArrayList<Listener>();
+
+ private final class Listener implements IBinder.DeathRecipient {
+ final IGpsStatusListener mListener;
+
+ Listener(IGpsStatusListener listener) {
+ mListener = listener;
+ }
+
+ public void binderDied() {
+ if (D) Log.d(TAG, "GPS status listener died");
+
+ synchronized(mListeners) {
+ mListeners.remove(this);
+ }
+ if (mListener != null) {
+ mListener.asBinder().unlinkToDeath(this, 0);
+ }
+ }
+ }
+
+
+ private final IGpsStatusProvider mGpsStatusProvider = new IGpsStatusProvider.Stub() {
+ public void addGpsStatusListener(IGpsStatusListener listener) throws RemoteException {
+ if (listener == null) {
+ throw new NullPointerException("listener is null in addGpsStatusListener");
+ }
+ synchronized(mListeners) {
+ IBinder binder = listener.asBinder();
+ int size = mListeners.size();
+ for (int i = 0; i < size; i++) {
+ Listener test = mListeners.get(i);
+ if (binder.equals(test.mListener.asBinder())) {
+ // listener already added
+ return;
+ }
+ }
+ Listener l = new Listener(listener);
+ binder.linkToDeath(l, 0);
+ mListeners.add(l);
+ }
+ }
+
+ public void removeGpsStatusListener(IGpsStatusListener listener) {
+ if (listener == null) {
+ throw new NullPointerException("listener is null in addGpsStatusListener");
+ }
+
+ synchronized(mListeners) {
+ IBinder binder = listener.asBinder();
+ Listener l = null;
+ int size = mListeners.size();
+ for (int i = 0; i < size && l == null; i++) {
+ Listener test = mListeners.get(i);
+ if (binder.equals(test.mListener.asBinder())) {
+ l = test;
+ }
+ }
+
+ if (l != null) {
+ mListeners.remove(l);
+ binder.unlinkToDeath(l, 0);
+ }
+ }
+ }
+ };
+
+ public IGpsStatusProvider getGpsStatusProvider() {
+ return mGpsStatusProvider;
+ }
+
+ public void addListener(int uid) {
+ synchronized (mWakeLock) {
+ mPendingListenerMessages++;
+ mWakeLock.acquire();
+ Message m = Message.obtain(mHandler, ADD_LISTENER);
+ m.arg1 = uid;
+ mHandler.sendMessage(m);
+ }
+ }
+
+ private void handleAddListener(int uid) {
+ synchronized(mListeners) {
+ if (mClientUids.indexOfKey(uid) >= 0) {
+ // Shouldn't be here -- already have this uid.
+ Log.w(TAG, "Duplicate add listener for uid " + uid);
+ return;
+ }
+ mClientUids.put(uid, 0);
+ if (mNavigating) {
+ try {
+ mBatteryStats.noteStartGps(uid);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException in addListener");
+ }
+ }
+ }
+ }
+
+ public void removeListener(int uid) {
+ synchronized (mWakeLock) {
+ mPendingListenerMessages++;
+ mWakeLock.acquire();
+ Message m = Message.obtain(mHandler, REMOVE_LISTENER);
+ m.arg1 = uid;
+ mHandler.sendMessage(m);
+ }
+ }
+
+ private void handleRemoveListener(int uid) {
+ synchronized(mListeners) {
+ if (mClientUids.indexOfKey(uid) < 0) {
+ // Shouldn't be here -- don't have this uid.
+ Log.w(TAG, "Unneeded remove listener for uid " + uid);
+ return;
+ }
+ mClientUids.delete(uid);
+ if (mNavigating) {
+ try {
+ mBatteryStats.noteStopGps(uid);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException in removeListener");
+ }
+ }
+ }
+ }
+}
diff --git a/services/java/com/android/server/location/NMEAParser.java b/services/java/com/android/server/location/NMEAParser.java
new file mode 100644
index 0000000..90bbe84
--- /dev/null
+++ b/services/java/com/android/server/location/NMEAParser.java
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2011 Cuong Bui
+ *
+ * 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.server.location;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.location.Location;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class NMEAParser {
+ private static final String TAG = "NMEAParser";
+ private static final String delim = ",";
+ // NMEA sentence pattern
+ private final Pattern sentencePattern = Pattern.compile("\\$([^*$]{5,})(\\*\\w{2})?");
+ private final SimpleDateFormat timeFormatter = new SimpleDateFormat("HHmmss.S");
+ private final TimeZone mLocalTZ = TimeZone.getDefault();
+
+ private HashMap<String,ParseInterface> parseMap = new HashMap<String,ParseInterface>();
+ private String provider;
+
+ private static final String BUNDLE_SATS = "satellites";
+ // for GPS SV statistics
+ private static final int MAX_SVS = 32;
+ public static final int EPHEMERIS_MASK = 0;
+ public static final int ALMANAC_MASK = 1;
+ public static final int USED_FOR_FIX_MASK = 2;
+
+ // preallocated arrays, to avoid memory allocation in reportStatus()
+ private int mSvs[] = new int[MAX_SVS];
+ private float mSnrs[] = new float[MAX_SVS];
+ private float mSvElevations[] = new float[MAX_SVS];
+ private float mSvAzimuths[] = new float[MAX_SVS];
+ private int mSvMasks[] = new int[3];
+ private int mSvCount;
+
+ private float PDOP = 0f;
+ private float HDOP = 0f;
+ private float VDOP = 0f;
+
+ private boolean isValid = false;
+ private long mFixDateTimeStamp = 0;
+ private double mFixLongitude = 0.0;
+ private double mFixLatitude = 0.0;
+ private float mFixAltitude = 0f;
+ private float mFixSpeed = 0f;
+ private float mFixBearing = 0f;
+ private float mFixAccuracy = 0f;
+ private int mFixSatsTracked=0;
+ private int mFixQuality = 0;
+
+ //horizontal estimated position error
+ private float HEPE_FACTOR = 4f;
+
+ // last fix timestamp. Is used to approximate and adjust gps mouse refresh rate.
+ private long mFixTimestampDelta=500;
+
+ private boolean mSatsReady = true;
+ private Location loc = new Location(provider);
+ private GregorianCalendar currCalendar = new GregorianCalendar();
+
+ /**
+ * @param prov Location provider name
+ */
+ public NMEAParser(String prov) {
+ // init parser map with all known parsers
+ parseMap.put("GPRMC", new GPRMCParser());
+ parseMap.put("GPGGA", new GPGGAParser());
+ parseMap.put("GPGSA", new GPGSAParser());
+ parseMap.put("GPGSV", new GPGSVParser());
+
+ provider = prov;
+ timeFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ }
+
+ private void updateTimeStamp(long in) {
+ if (mFixDateTimeStamp != 0 && in != mFixDateTimeStamp) {
+ mFixTimestampDelta = in - mFixDateTimeStamp;
+ if (mFixTimestampDelta < 100) mFixTimestampDelta = 100;
+ if (mFixTimestampDelta > 1000) mFixTimestampDelta = 1000;
+ }
+ mFixDateTimeStamp = in;
+ }
+
+ public long getApproximatedRefreshRate() {
+ return mFixTimestampDelta;
+ }
+ /**
+ * @return if nmea sentence are valid then true
+ */
+ public boolean isValid() {
+ return isValid;
+ }
+
+ /**
+ * resets fix variables
+ */
+ public void reset() {
+ mFixLongitude = 0.0;
+ mFixLatitude = 0.0;
+ mFixAltitude = 0f;
+ mFixSpeed = 0f;
+ mFixAccuracy = 0f;
+ mFixQuality = 0;
+ mFixSatsTracked = 0;
+ }
+
+ private void resetSats() {
+ mSvCount = 0;
+ java.util.Arrays.fill(mSvs, 0);
+ java.util.Arrays.fill(mSnrs, 0f);
+ java.util.Arrays.fill(mSvElevations, 0f);
+ java.util.Arrays.fill(mSvAzimuths, 0f);
+ }
+
+
+ /**
+ * @return a Location object if valid null otherwise
+ */
+ public Location getLocation() {
+ loc.reset();
+ if (mFixDateTimeStamp != 0) loc.setTime(mFixDateTimeStamp);
+ loc.setLatitude(mFixLatitude);
+ loc.setLongitude(mFixLongitude);
+ Bundle extras = new Bundle();
+ extras.putInt(BUNDLE_SATS, mFixSatsTracked);
+ loc.setExtras(extras);
+ loc.setAccuracy(mFixAccuracy);
+ loc.setAltitude(mFixAltitude);
+ loc.setSpeed(mFixSpeed);
+ loc.setBearing(mFixBearing);
+ return loc;
+ }
+
+ /**
+ * @param time UTC time
+ * @return nr seconds since 1970
+ */
+ private long parseTimeToDate(String time) {
+ try {
+ Date btTime = timeFormatter.parse(time);
+ //System.currentTimeMillis()
+ GregorianCalendar cc = new GregorianCalendar();
+ cc.setTimeInMillis(System.currentTimeMillis());
+ currCalendar.setTimeInMillis(btTime.getTime() + mLocalTZ.getRawOffset());
+ currCalendar.set(cc.get(Calendar.YEAR), cc.get(Calendar.MONTH),
+ cc.get(Calendar.DAY_OF_WEEK));
+ return currCalendar.getTimeInMillis();
+ } catch (ParseException e) {
+ Log.e(TAG, "Could not parse: " + time);
+ return 0;
+ }
+ }
+
+ private int parseStringToInt(String str) {
+ if (TextUtils.isEmpty(str))
+ return 0;
+ int res = 0;
+ try {
+ res = Integer.parseInt(str);
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ }
+ return res;
+ }
+
+ private float parseStringToFloat(String str) {
+ if (TextUtils.isEmpty(str))
+ return 0.0f;
+
+ float res = 0.0f;
+ try {
+ res = Float.parseFloat(str);
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ }
+ return res;
+ }
+
+ /**
+ * @param in Longitude/Latitude
+ * @param orientation N,W,S,E
+ * @return The double representation of a Longitude/Latitude
+ */
+ private double parseCoordinate(String in, String orientation) {
+ // dec = deg + mins.sec/60
+ double c = Double.parseDouble(in);
+ int deg = (int) (c/100);
+ double res = deg + (c - deg*100.0)*0.016666666666667;
+ if ("S".equalsIgnoreCase(orientation) || "W".equalsIgnoreCase(orientation)) return -res;
+ return res;
+ }
+
+ private float parseSpeedInKnots(String str) {
+ float res = 0.0f;
+ res = Float.parseFloat(str) * 0.514444444f;
+ return res;
+ }
+
+ private float parseSpeedInKMH(String str) {
+ float res = 0.0f;
+ res = Float.parseFloat(str) * 0.277777778f;
+ return res;
+ }
+
+ /**
+ * Interface that all sentence parsers have to implement.
+ *
+ *Every sentence is implemented as a seperate class. The Nmea
+ *parser will select the correct parser based on the sentence identifier.
+ *It will get or instantiate a parser to do the job.
+ */
+ private interface ParseInterface {
+ public void parse(String sentence);
+ }
+
+ public class GPRMCParser implements ParseInterface {
+ /*
+ * $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
+
+Where:
+ RMC Recommended Minimum sentence C
+ 123519 Fix taken at 12:35:19 UTC
+ A Status A=active or V=Void.
+ 4807.038,N Latitude 48 deg 07.038' N
+ 01131.000,E Longitude 11 deg 31.000' E
+ 022.4 Speed over the ground in knots
+ 084.4 Track angle in degrees True
+ 230394 Date - 23rd of March 1994
+ 003.1,W Magnetic Variation
+ *6A The checksum data, always begins with *
+
+ */
+ @Override
+ public void parse(String sentence) {
+ String[] tmp = sentence.split(delim);
+ if (tmp.length > 3) {
+ updateTimeStamp(parseTimeToDate(tmp[1]));
+ if (!"A".equals(tmp[2])) {
+ return;
+ }
+ mFixLatitude = parseCoordinate(tmp[3], tmp[4]);
+ mFixLongitude = parseCoordinate(tmp[5], tmp[6]);
+ mFixSpeed = parseSpeedInKnots(tmp[7]);
+ mFixBearing = parseStringToFloat(tmp[8]);
+ }
+ }
+ }
+
+ public class GPGGAParser implements ParseInterface {
+ /*
+ $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
+Where:
+ GGA Global Positioning System Fix Data
+ 123519 Fix taken at 12:35:19 UTC
+ 4807.038,N Latitude 48 deg 07.038' N
+ 01131.000,E Longitude 11 deg 31.000' E
+ 1 Fix quality: 0 = invalid
+ 1 = GPS fix (SPS)
+ 2 = DGPS fix
+ 3 = PPS fix
+ 4 = Real Time Kinematic
+ 5 = Float RTK
+ 6 = estimated (dead reckoning) (2.3 feature)
+ 7 = Manual input mode
+ 8 = Simulation mode
+ 08 Number of satellites being tracked
+ 0.9 Horizontal dilution of position
+ 545.4,M Altitude, Meters, above mean sea level
+ 46.9,M Height of geoid (mean sea level) above WGS84
+ ellipsoid
+ (empty field) time in seconds since last DGPS update
+ (empty field) DGPS station ID number
+ *47 the checksum data, always begins with * */
+ @Override
+ public void parse(String sentence) {
+ String[] tmp = sentence.split(delim);
+ if (tmp.length > 7) {
+ // always parse timestamp
+ updateTimeStamp(parseTimeToDate(tmp[1]));
+ mFixQuality = Integer.parseInt(tmp[6]);
+ if (mFixQuality == 0) {
+ // return invalid location
+ isValid = false;
+ return;
+ }
+ mFixLatitude = parseCoordinate(tmp[2], tmp[3]);
+ mFixLongitude = parseCoordinate(tmp[4], tmp[5]);
+ mFixSatsTracked = parseStringToInt(tmp[7]);
+ mFixAccuracy = parseStringToFloat(tmp[8]) * HEPE_FACTOR;
+ mFixAltitude = parseStringToFloat(tmp[9]);
+ isValid = true;
+ }
+ }
+ }
+
+ public class GPGSAParser implements ParseInterface {
+ /*
+ $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39
+
+Where:
+ GSA Satellite status
+ A Auto selection of 2D or 3D fix (M = manual)
+ 3 3D fix - values include: 1 = no fix
+ 2 = 2D fix
+ 3 = 3D fix
+ 04,05... PRNs of satellites used for fix (space for 12)
+ 2.5 PDOP (dilution of precision)
+ 1.3 Horizontal dilution of precision (HDOP)
+ 2.1 Vertical dilution of precision (VDOP)
+ *39 the checksum data, always begins with *
+ * */
+ @Override
+ public void parse(String sentence) {
+ String[] tmp = sentence.split(delim);
+ if (tmp.length >= 16) {
+ if ("1".equals(tmp[2])) {
+ // return invalid location or invalid sentence
+ return;
+ }
+ for (int i=3; i < 15; i++) {
+ // tag sats used for fix
+ int sat = parseStringToInt(tmp[i]);
+ if (sat > 0) mSvMasks[USED_FOR_FIX_MASK] |= (1 << (sat - 1));
+ }
+ if (tmp.length > 15)
+ PDOP = parseStringToFloat(tmp[15]);
+ if (tmp.length > 16)
+ HDOP = parseStringToFloat(tmp[16]);
+ if (tmp.length > 17)
+ VDOP = parseStringToFloat(tmp[17]);
+ }
+ }
+ }
+
+ /**
+ * Parse sats information. Use same structure as internal GPS provider
+ *
+ */
+ public class GPGSVParser implements ParseInterface {
+ /*
+ $GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75
+
+Where:
+ GSV Satellites in view
+ 2 Number of sentences for full data
+ 1 sentence 1 of 2
+ 08 Number of satellites in view
+
+ 01 Satellite PRN number
+ 40 Elevation, degrees
+ 083 Azimuth, degrees
+ 46 SNR - higher is better
+ for up to 4 satellites per sentence
+ *75 the checksum data, always begins with *
+ */
+ @Override
+ public void parse(String sentence) {
+ String[] tmp = sentence.split(delim);
+ if (tmp.length > 4) {
+ mSvCount = parseStringToInt(tmp[3]);
+ if (mSvCount == 0) {
+ return;
+ }
+ int totalSentences = parseStringToInt(tmp[1]);
+ int currSentence = parseStringToInt(tmp[2]);
+
+ if (mSatsReady) {
+ resetSats();
+ mSatsReady = false;
+ } else if ((currSentence == totalSentences) && !mSatsReady) {
+ // tag data as dirty when we have parsed the last part
+ mSatsReady = true;
+ }
+ int idx = 0;
+ while ((currSentence <= totalSentences) && (idx < 4)) {
+ int offset = idx<<2;
+ int base_offset = (currSentence-1)<<2;
+ if (offset+4 < tmp.length)
+ mSvs[base_offset + idx] = parseStringToInt(tmp[4 + offset]);
+ if (offset+5 < tmp.length)
+ mSvElevations[base_offset + idx] = parseStringToInt(tmp[5 + offset]);
+ if (offset+6 < tmp.length)
+ mSvAzimuths[base_offset + idx] = parseStringToInt(tmp[6 + offset]);
+ if (offset+7 < tmp.length)
+ mSnrs[base_offset + idx] = parseStringToInt(tmp[7 + offset]);
+ idx++;
+ }
+ }
+ }
+ }
+
+ /**
+ * Using non static dynamic innerclass instantiation.
+ * @param sid sentence identifier
+ * @return parser associated with the sid
+ */
+ private ParseInterface getParser(String sid) {
+ if (parseMap.containsKey(sid)) {
+ return parseMap.get(sid);
+ } else {
+ Log.d(TAG, "Could not instantiate " + sid + "parser");
+ }
+ return null;
+ }
+
+ /**
+ * @param in nmea sentence
+ * @return String representing checksum of the input
+ */
+ private String computeChecksum(String in) {
+ byte result = 0;
+ char[] chars = in.toCharArray();
+ for (int i=0; i < chars.length; i++)
+ result ^= (byte) chars[i];
+ return String.format("%02X", result);
+ }
+
+ public boolean parseNMEALine(String sentence) {
+ Matcher m = sentencePattern.matcher(sentence);
+ if (m.matches()) {
+ String nmeaSentence = m.group(1);
+ String command = nmeaSentence.substring(0, 5);
+ String checksum = m.group(2);
+ if (checksum != null) {
+ // checksums are optional
+ // strip off *, checksum always have length 3 here. else the regex will not match
+ checksum = checksum.substring(1, 3);
+ if (!computeChecksum(nmeaSentence).equals(checksum)) {
+ Log.w(TAG, "skipping sentence: " + sentence + " due to checksum error "
+ + checksum + " - " + computeChecksum(nmeaSentence));
+ return false;
+ }
+ }
+ ParseInterface parser = getParser(command);
+ if (parser != null) {
+ try {
+ parser.parse(nmeaSentence);
+ } catch (Exception e) {
+ // catch exception thrown by parsers
+ // mostly bad input causing out of bounds
+ Log.e(TAG,nmeaSentence, e);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public int getmSvCount() {
+ return mSvCount;
+ }
+
+ public float getPDOP() {
+ return PDOP;
+ }
+
+ public float getHDOP() {
+ return HDOP;
+ }
+
+ public float getVDOP() {
+ return VDOP;
+ }
+
+ public long getmFixDate() {
+ return mFixDateTimeStamp;
+ }
+
+ public double getmFixLongitude() {
+ return mFixLongitude;
+ }
+
+ public double getmFixLatitude() {
+ return mFixLatitude;
+ }
+
+ public float getmFixAltitude() {
+ return mFixAltitude;
+ }
+
+ public float getmFixSpeed() {
+ return mFixSpeed;
+ }
+
+ public float getmFixAccuracy() {
+ return mFixAccuracy;
+ }
+
+ public int getmFixQuality() {
+ return mFixQuality;
+ }
+ public int[] getmSvs() {
+ return mSvs;
+ }
+
+ public float[] getmSnrs() {
+ return mSnrs;
+ }
+
+ public float[] getmSvElevations() {
+ return mSvElevations;
+ }
+
+ public float[] getmSvAzimuths() {
+ return mSvAzimuths;
+ }
+
+ public int[] getmSvMasks() {
+ return mSvMasks;
+ }
+
+ public int getmFixSatsTracked() {
+ return mFixSatsTracked;
+ }
+
+ public boolean isSatdataReady() {
+ return mSatsReady;
+ }
+}