gps: Add GpsStatus.NmeaListener interface for receiving NMEA sentences.

NMEA sentences are passed from the GPS engine to the GpsLocationProvider.
They are then sent via the IGpsStatusListener binder interface to clients
using the same path as the other GPS status information.

Signed-off-by: Mike Lockwood <lockwood@android.com>
diff --git a/core/jni/android_location_GpsLocationProvider.cpp b/core/jni/android_location_GpsLocationProvider.cpp
index bf0bd65..90a0487 100755
--- a/core/jni/android_location_GpsLocationProvider.cpp
+++ b/core/jni/android_location_GpsLocationProvider.cpp
@@ -32,6 +32,7 @@
 static jmethodID method_reportStatus;
 static jmethodID method_reportSvStatus;
 static jmethodID method_reportAGpsStatus;
+static jmethodID method_reportNmea;
 static jmethodID method_xtraDownloadRequest;
 
 static const GpsInterface* sGpsInterface = NULL;
@@ -44,12 +45,23 @@
 static GpsSvStatus  sGpsSvStatus;
 static AGpsStatus   sAGpsStatus;
 
+// buffer for NMEA data
+#define NMEA_SENTENCE_LENGTH    100
+#define NMEA_SENTENCE_COUNT     40
+struct NmeaSentence {
+    GpsUtcTime  timestamp;
+    char        nmea[NMEA_SENTENCE_LENGTH];
+};
+static NmeaSentence sNmeaBuffer[NMEA_SENTENCE_LENGTH];
+static int mNmeaSentenceCount = 0;
+
 // a copy of the data shared by android_location_GpsLocationProvider_wait_for_event
 // and android_location_GpsLocationProvider_read_status
 static GpsLocation  sGpsLocationCopy;
 static GpsStatus    sGpsStatusCopy;
 static GpsSvStatus  sGpsSvStatusCopy;
 static AGpsStatus   sAGpsStatusCopy;
+static NmeaSentence sNmeaBufferCopy[NMEA_SENTENCE_LENGTH];
 
 enum CallbackType {
     kLocation = 1,
@@ -58,6 +70,7 @@
     kAGpsStatus = 8,
     kXtraDownloadRequest = 16,
     kDisableRequest = 32,
+    kNmeaAvailable = 64,
 }; 
 static int sPendingCallbacks;
 
@@ -96,6 +109,30 @@
     pthread_mutex_unlock(&sEventMutex);
 }
 
+static void nmea_callback(GpsUtcTime timestamp, const char* nmea, int length)
+{
+    pthread_mutex_lock(&sEventMutex);
+
+    if (length >= NMEA_SENTENCE_LENGTH) {
+        LOGE("NMEA data too long in nmea_callback (length = %d)\n", length);
+        length = NMEA_SENTENCE_LENGTH - 1;
+    }
+    if (mNmeaSentenceCount >= NMEA_SENTENCE_COUNT) {
+        LOGE("NMEA data overflowed buffer\n");
+        pthread_mutex_unlock(&sEventMutex);
+        return;
+    }
+
+    sPendingCallbacks |= kNmeaAvailable;
+    sNmeaBuffer[mNmeaSentenceCount].timestamp = timestamp;
+    memcpy(sNmeaBuffer[mNmeaSentenceCount].nmea, nmea, length);
+    sNmeaBuffer[mNmeaSentenceCount].nmea[length] = 0;
+    mNmeaSentenceCount++;
+
+    pthread_cond_signal(&sEventCond);
+    pthread_mutex_unlock(&sEventMutex);
+}
+
 static void agps_status_callback(AGpsStatus* agps_status)
 {
     pthread_mutex_lock(&sEventMutex);
@@ -111,6 +148,7 @@
     location_callback,
     status_callback,
     sv_status_callback,
+    nmea_callback
 };
 
 static void
@@ -135,6 +173,7 @@
     method_reportStatus = env->GetMethodID(clazz, "reportStatus", "(I)V");
     method_reportSvStatus = env->GetMethodID(clazz, "reportSvStatus", "()V");
     method_reportAGpsStatus = env->GetMethodID(clazz, "reportAGpsStatus", "(II)V");
+    method_reportNmea = env->GetMethodID(clazz, "reportNmea", "(IJ)V");
     method_xtraDownloadRequest = env->GetMethodID(clazz, "xtraDownloadRequest", "()V");
 }
 
@@ -200,13 +239,21 @@
     // copy and clear the callback flags
     int pendingCallbacks = sPendingCallbacks;
     sPendingCallbacks = 0;
+    int nmeaSentenceCount = mNmeaSentenceCount;
+    mNmeaSentenceCount = 0;
     
     // copy everything and unlock the mutex before calling into Java code to avoid the possibility
     // of timeouts in the GPS engine.
-    memcpy(&sGpsLocationCopy, &sGpsLocation, sizeof(sGpsLocationCopy));
-    memcpy(&sGpsStatusCopy, &sGpsStatus, sizeof(sGpsStatusCopy));
-    memcpy(&sGpsSvStatusCopy, &sGpsSvStatus, sizeof(sGpsSvStatusCopy));
-    memcpy(&sAGpsStatusCopy, &sAGpsStatus, sizeof(sAGpsStatusCopy));
+    if (pendingCallbacks & kLocation)
+        memcpy(&sGpsLocationCopy, &sGpsLocation, sizeof(sGpsLocationCopy));
+    if (pendingCallbacks & kStatus)
+        memcpy(&sGpsStatusCopy, &sGpsStatus, sizeof(sGpsStatusCopy));
+    if (pendingCallbacks & kSvStatus)
+        memcpy(&sGpsSvStatusCopy, &sGpsSvStatus, sizeof(sGpsSvStatusCopy));
+    if (pendingCallbacks & kAGpsStatus)
+        memcpy(&sAGpsStatusCopy, &sAGpsStatus, sizeof(sAGpsStatusCopy));
+    if (pendingCallbacks & kNmeaAvailable)
+        memcpy(&sNmeaBufferCopy, &sNmeaBuffer, nmeaSentenceCount * sizeof(sNmeaBuffer[0]));
     pthread_mutex_unlock(&sEventMutex);   
 
     if (pendingCallbacks & kLocation) { 
@@ -225,6 +272,11 @@
     if (pendingCallbacks & kAGpsStatus) {
         env->CallVoidMethod(obj, method_reportAGpsStatus, sAGpsStatusCopy.type, sAGpsStatusCopy.status);
     }  
+    if (pendingCallbacks & kNmeaAvailable) {
+        for (int i = 0; i < nmeaSentenceCount; i++) {
+            env->CallVoidMethod(obj, method_reportNmea, i, sNmeaBuffer[i].timestamp);
+        }
+    }
     if (pendingCallbacks & kXtraDownloadRequest) {    
         env->CallVoidMethod(obj, method_xtraDownloadRequest);
     }
@@ -264,6 +316,21 @@
     return num_svs;
 }
 
+static jint android_location_GpsLocationProvider_read_nmea(JNIEnv* env, jobject obj, jint index, jbyteArray nmeaArray, jint buffer_size)
+{
+    // this should only be called from within a call to reportStatus, so we don't need to lock here
+
+    jbyte* nmea = env->GetByteArrayElements(nmeaArray, 0);
+
+    int length = strlen(sNmeaBuffer[index].nmea);
+    if (length > buffer_size)
+        length = buffer_size;
+    memcpy(nmea, sNmeaBuffer[index].nmea, length);
+
+    env->ReleaseByteArrayElements(nmeaArray, nmea, 0);
+    return length;
+}
+
 static void android_location_GpsLocationProvider_inject_time(JNIEnv* env, jobject obj, jlong time, 
         jlong timeReference, jint uncertainty)
 {
@@ -360,6 +427,7 @@
     {"native_delete_aiding_data", "(I)V", (void*)android_location_GpsLocationProvider_delete_aiding_data},
     {"native_wait_for_event", "()V", (void*)android_location_GpsLocationProvider_wait_for_event},
     {"native_read_sv_status", "([I[F[F[F[I)I", (void*)android_location_GpsLocationProvider_read_sv_status},
+    {"native_read_nmea", "(I[BI)I", (void*)android_location_GpsLocationProvider_read_nmea},
     {"native_inject_time", "(JJI)V", (void*)android_location_GpsLocationProvider_inject_time},
     {"native_inject_location", "(DDF)V", (void*)android_location_GpsLocationProvider_inject_location},
     {"native_supports_xtra", "()Z", (void*)android_location_GpsLocationProvider_supports_xtra},
diff --git a/location/java/android/location/GpsStatus.java b/location/java/android/location/GpsStatus.java
index 2cda7fa..883ee4f 100644
--- a/location/java/android/location/GpsStatus.java
+++ b/location/java/android/location/GpsStatus.java
@@ -115,6 +115,15 @@
         void onGpsStatusChanged(int event);
     }
 
+    /**
+     * Used for receiving NMEA data from the GPS.
+     *
+     * {@hide}
+     */
+    public interface NmeaListener {
+        void onNmeaReceived(long timestamp, String nmea);
+    }
+
     GpsStatus() {
         for (int i = 0; i < mSatellites.length; i++) {
             mSatellites[i] = new GpsSatellite(i + 1);
diff --git a/location/java/android/location/IGpsStatusListener.aidl b/location/java/android/location/IGpsStatusListener.aidl
index 5dc0fe8..62b1c6b 100644
--- a/location/java/android/location/IGpsStatusListener.aidl
+++ b/location/java/android/location/IGpsStatusListener.aidl
@@ -29,4 +29,5 @@
     void onSvStatusChanged(int svCount, in int[] prns, in float[] snrs, 
             in float[] elevations, in float[] azimuths, 
             int ephemerisMask, int almanacMask, int usedInFixMask);
+    void onNmeaReceived(long timestamp, String nmea);
 }
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index ca16f19..8f17e78 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -51,6 +51,8 @@
     private ILocationManager mService;
     private final HashMap<GpsStatus.Listener, GpsStatusListenerTransport> mGpsStatusListeners =
             new HashMap<GpsStatus.Listener, GpsStatusListenerTransport>();
+    private final HashMap<GpsStatus.NmeaListener, GpsStatusListenerTransport> mNmeaListeners =
+            new HashMap<GpsStatus.NmeaListener, GpsStatusListenerTransport>();
     private final GpsStatus mGpsStatus = new GpsStatus();
 
     /**
@@ -1123,49 +1125,103 @@
     private class GpsStatusListenerTransport extends IGpsStatusListener.Stub {
 
         private final GpsStatus.Listener mListener;
+        private final GpsStatus.NmeaListener mNmeaListener;
+
+        // This must not equal any of the GpsStatus event IDs
+        private static final int NMEA_RECEIVED = 1000;
+
+        private class Nmea {
+            long mTimestamp;
+            String mNmea;
+
+            Nmea(long timestamp, String nmea) {
+                mTimestamp = timestamp;
+                mNmea = nmea;
+            }
+        }
+        private ArrayList<Nmea> mNmeaBuffer;
 
         GpsStatusListenerTransport(GpsStatus.Listener listener) {
             mListener = listener;
+            mNmeaListener = null;
+        }
+
+        GpsStatusListenerTransport(GpsStatus.NmeaListener listener) {
+            mNmeaListener = listener;
+            mListener = null;
+            mNmeaBuffer = new ArrayList<Nmea>();
         }
 
         public void onGpsStarted() {
-            Message msg = Message.obtain();
-            msg.what = GpsStatus.GPS_EVENT_STARTED;
-            mGpsHandler.sendMessage(msg);
+            if (mListener != null) {
+                Message msg = Message.obtain();
+                msg.what = GpsStatus.GPS_EVENT_STARTED;
+                mGpsHandler.sendMessage(msg);
+            }
         }
 
         public void onGpsStopped() {
-            Message msg = Message.obtain();
-            msg.what = GpsStatus.GPS_EVENT_STOPPED;
-            mGpsHandler.sendMessage(msg);
+            if (mListener != null) {
+                Message msg = Message.obtain();
+                msg.what = GpsStatus.GPS_EVENT_STOPPED;
+                mGpsHandler.sendMessage(msg);
+            }
         }
 
         public void onFirstFix(int ttff) {
-            mGpsStatus.setTimeToFirstFix(ttff);
-            Message msg = Message.obtain();
-            msg.what = GpsStatus.GPS_EVENT_FIRST_FIX;
-            mGpsHandler.sendMessage(msg);
+            if (mListener != null) {
+                mGpsStatus.setTimeToFirstFix(ttff);
+                Message msg = Message.obtain();
+                msg.what = GpsStatus.GPS_EVENT_FIRST_FIX;
+                mGpsHandler.sendMessage(msg);
+            }
         }
 
         public void onSvStatusChanged(int svCount, int[] prns, float[] snrs,
                 float[] elevations, float[] azimuths, int ephemerisMask,
                 int almanacMask, int usedInFixMask) {
-            mGpsStatus.setStatus(svCount, prns, snrs, elevations, azimuths,
-                    ephemerisMask, almanacMask, usedInFixMask);
+            if (mListener != null) {
+                mGpsStatus.setStatus(svCount, prns, snrs, elevations, azimuths,
+                        ephemerisMask, almanacMask, usedInFixMask);
 
-            Message msg = Message.obtain();
-            msg.what = GpsStatus.GPS_EVENT_SATELLITE_STATUS;
-            // remove any SV status messages already in the queue
-            mGpsHandler.removeMessages(GpsStatus.GPS_EVENT_SATELLITE_STATUS);
-            mGpsHandler.sendMessage(msg);
+                Message msg = Message.obtain();
+                msg.what = GpsStatus.GPS_EVENT_SATELLITE_STATUS;
+                // remove any SV status messages already in the queue
+                mGpsHandler.removeMessages(GpsStatus.GPS_EVENT_SATELLITE_STATUS);
+                mGpsHandler.sendMessage(msg);
+            }
+        }
+
+        public void onNmeaReceived(long timestamp, String nmea) {
+            if (mNmeaListener != null) {
+                synchronized (mNmeaBuffer) {
+                    mNmeaBuffer.add(new Nmea(timestamp, nmea));
+                }
+                Message msg = Message.obtain();
+                msg.what = NMEA_RECEIVED;
+                // remove any NMEA_RECEIVED messages already in the queue
+                mGpsHandler.removeMessages(NMEA_RECEIVED);
+                mGpsHandler.sendMessage(msg);
+            }
         }
 
         private final Handler mGpsHandler = new Handler() {
             @Override
             public void handleMessage(Message msg) {
-                // synchronize on mGpsStatus to ensure the data is copied atomically.
-                synchronized(mGpsStatus) {
-                    mListener.onGpsStatusChanged(msg.what);
+                if (msg.what == NMEA_RECEIVED) {
+                    synchronized (mNmeaBuffer) {
+                        int length = mNmeaBuffer.size();
+                        for (int i = 0; i < length; i++) {
+                            Nmea nmea = mNmeaBuffer.get(i);
+                            mNmeaListener.onNmeaReceived(nmea.mTimestamp, nmea.mNmea);
+                        }
+                        mNmeaBuffer.clear();
+                    }
+                } else {
+                    // synchronize on mGpsStatus to ensure the data is copied atomically.
+                    synchronized(mGpsStatus) {
+                        mListener.onGpsStatusChanged(msg.what);
+                    }
                 }
             }
         };
@@ -1217,6 +1273,56 @@
         }
     }
 
+    /**
+     * Adds an NMEA listener.
+     *
+     * @param listener NMEA listener object to register
+     *
+     * @return true if the listener was successfully added
+     *
+     * @throws SecurityException if the ACCESS_FINE_LOCATION permission is not present
+     *
+     * {@hide}
+     */
+    public boolean addNmeaListener(GpsStatus.NmeaListener listener) {
+        boolean result;
+
+        if (mNmeaListeners.get(listener) != null) {
+            // listener is already registered
+            return true;
+        }
+        try {
+            GpsStatusListenerTransport transport = new GpsStatusListenerTransport(listener);
+            result = mService.addGpsStatusListener(transport);
+            if (result) {
+                mNmeaListeners.put(listener, transport);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in registerGpsStatusListener: ", e);
+            result = false;
+        }
+
+        return result;
+    }
+
+    /**
+     * Removes an NMEA listener.
+     *
+     * @param listener NMEA listener object to remove
+     *
+     * {@hide}
+     */
+    public void removeNmeaListener(GpsStatus.NmeaListener listener) {
+        try {
+            GpsStatusListenerTransport transport = mNmeaListeners.remove(listener);
+            if (transport != null) {
+                mService.removeGpsStatusListener(transport);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in unregisterGpsStatusListener: ", e);
+        }
+    }
+
      /**
      * Retrieves information about the current status of the GPS engine.
      * This should only be called from the {@link GpsStatus.Listener#onGpsStatusChanged}
diff --git a/location/java/com/android/internal/location/GpsLocationProvider.java b/location/java/com/android/internal/location/GpsLocationProvider.java
index aaac192..bdef01f 100755
--- a/location/java/com/android/internal/location/GpsLocationProvider.java
+++ b/location/java/com/android/internal/location/GpsLocationProvider.java
@@ -1014,6 +1014,32 @@
         }
     }
 
+    /**
+     * called from native code to report NMEA data received
+     */
+    private void reportNmea(int index, long timestamp) {
+        synchronized(mListeners) {
+            int size = mListeners.size();
+            if (size > 0) {
+                // don't bother creating the String if we have no listeners
+                int length = native_read_nmea(index, mNmeaBuffer, mNmeaBuffer.length);
+                String nmea = new String(mNmeaBuffer, 0, length);
+
+                for (int i = 0; i < size; i++) {
+                    Listener listener = mListeners.get(i);
+                    try {
+                        listener.mListener.onNmeaReceived(timestamp, nmea);
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "RemoteException in reportNmea");
+                        mListeners.remove(listener);
+                        // adjust for size of list changing
+                        size--;
+                    }
+                }
+            }
+        }
+    }
+
     private void xtraDownloadRequest() {
         if (Config.LOGD) Log.d(TAG, "xtraDownloadRequest");
         if (mNetworkThread != null) {
@@ -1194,6 +1220,8 @@
     private float mSvAzimuths[] = new float[MAX_SVS];
     private int mSvMasks[] = new int[3];
     private int mSvCount;
+    // preallocated to avoid memory allocation in reportNmea()
+    private byte[] mNmeaBuffer = new byte[120];
 
     static { class_init_native(); }
     private static native void class_init_native();
@@ -1211,6 +1239,7 @@
     // mask[0] is ephemeris mask and mask[1] is almanac mask
     private native int native_read_sv_status(int[] svs, float[] snrs,
             float[] elevations, float[] azimuths, int[] masks);
+    private native int native_read_nmea(int index, byte[] buffer, int bufferSize);
     private native void native_inject_location(double latitude, double longitude, float accuracy);
 
     // XTRA Support