Local geomagnetic field update to sensor

Local geomagnetic field strength, declination, inclination information
is useful for magnetometer calibration. It also benefits rotation
vector sensor implementation as it gives a baseline that aids detecting
magnetic field disturbance.

Bug: 30958130
Test: Tested with marlin. Modified hal implementation can get local
      geomagnetic field.

Change-Id: I373fe74d5a091a3adb80ff3c61e441edcf5a253b
diff --git a/services/core/java/com/android/server/SensorNotificationService.java b/services/core/java/com/android/server/SensorNotificationService.java
index 0610464..7f5befa 100644
--- a/services/core/java/com/android/server/SensorNotificationService.java
+++ b/services/core/java/com/android/server/SensorNotificationService.java
@@ -20,25 +20,46 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.hardware.GeomagneticField;
 import android.hardware.Sensor;
+import android.hardware.SensorAdditionalInfo;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.Slog;
 
-public class SensorNotificationService extends SystemService implements SensorEventListener {
-    //TODO: set DBG to false or remove Slog before release
-    private static final boolean DBG = true;
+public class SensorNotificationService extends SystemService
+        implements SensorEventListener, LocationListener {
+    private static final boolean DBG = false;
     private static final String TAG = "SensorNotificationService";
-    private Context mContext;
 
+    private static final long MINUTE_IN_MS = 60 * 1000;
+    private static final long KM_IN_M = 1000;
+
+    private static final long LOCATION_MIN_TIME = 30 * MINUTE_IN_MS;
+    private static final long LOCATION_MIN_DISTANCE = 100 * KM_IN_M;
+
+    private static final String PROPERTY_USE_MOCKED_LOCATION =
+            "sensor.notification.use_mocked"; // max key length is 32
+
+    private static final long MILLIS_2010_1_1 = 1262358000000l;
+
+    private Context mContext;
     private SensorManager mSensorManager;
+    private LocationManager mLocationManager;
     private Sensor mMetaSensor;
 
+    // for rate limiting
+    private long mLocalGeomagneticFieldUpdateTime = -LOCATION_MIN_TIME;
+
     public SensorNotificationService(Context context) {
         super(context);
         mContext = context;
@@ -50,7 +71,6 @@
 
     public void onBootPhase(int phase) {
         if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
-            // start
             mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
             mMetaSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_DYNAMIC_SENSOR_META);
             if (mMetaSensor == null) {
@@ -60,13 +80,28 @@
                         SensorManager.SENSOR_DELAY_FASTEST);
             }
         }
+
+        if (phase == PHASE_BOOT_COMPLETED) {
+            // LocationManagerService is initialized after PHASE_THIRD_PARTY_APPS_CAN_START
+            mLocationManager =
+                    (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+            if (mLocationManager == null) {
+                if (DBG) Slog.d(TAG, "Cannot obtain location service.");
+            } else {
+                mLocationManager.requestLocationUpdates(
+                        LocationManager.PASSIVE_PROVIDER,
+                        LOCATION_MIN_TIME,
+                        LOCATION_MIN_DISTANCE,
+                        this);
+            }
+        }
     }
 
     private void broadcastDynamicSensorChanged() {
         Intent i = new Intent(Intent.ACTION_DYNAMIC_SENSOR_CHANGED);
         i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); // avoid waking up manifest receivers
         mContext.sendBroadcastAsUser(i, UserHandle.ALL);
-        if (DBG) Slog.d(TAG, "DYNS sent dynamic sensor broadcast");
+        if (DBG) Slog.d(TAG, "dynamic sensor broadcast sent");
     }
 
     @Override
@@ -77,8 +112,62 @@
     }
 
     @Override
-    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    public void onLocationChanged(Location location) {
+        if (DBG) Slog.d(TAG, String.format(
+                "Location is (%f, %f), h %f, acc %f, mocked %b",
+                location.getLatitude(), location.getLongitude(),
+                location.getAltitude(), location.getAccuracy(),
+                location.isFromMockProvider()));
 
+        // lat long == 0 usually means invalid location
+        if (location.getLatitude() == 0 && location.getLongitude() == 0) {
+            return;
+        }
+
+        // update too often, ignore
+        if (SystemClock.elapsedRealtime() - mLocalGeomagneticFieldUpdateTime < 10 * MINUTE_IN_MS) {
+            return;
+        }
+
+        long time = System.currentTimeMillis();
+        // Mocked location should not be used. Except in test, only use mocked location
+        // Wrong system clock also gives bad values so ignore as well.
+        if (useMockedLocation() == location.isFromMockProvider() || time < MILLIS_2010_1_1) {
+            return;
+        }
+
+        GeomagneticField field = new GeomagneticField(
+                (float) location.getLatitude(), (float) location.getLongitude(),
+                (float) location.getAltitude(), time);
+        if (DBG) Slog.d(TAG, String.format(
+                "Nominal mag field, norm %fuT, decline %f deg, incline %f deg",
+                field.getFieldStrength() / 1000, field.getDeclination(), field.getInclination()));
+
+        try {
+            SensorAdditionalInfo info = SensorAdditionalInfo.createLocalGeomagneticField(
+                        field.getFieldStrength() / 1000, // convert from nT to uT
+                        (float)(field.getDeclination() * Math.PI / 180), // from degree to rad
+                        (float)(field.getInclination() * Math.PI / 180)); // from degree to rad
+            if (info != null) {
+                mSensorManager.setOperationParameter(info);
+                mLocalGeomagneticFieldUpdateTime = SystemClock.elapsedRealtime();
+            }
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, "Invalid local geomagnetic field, ignore.");
+        }
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+    @Override
+    public void onStatusChanged(String provider, int status, Bundle extras) {}
+    @Override
+    public void onProviderEnabled(String provider) {}
+    @Override
+    public void onProviderDisabled(String provider) {}
+
+    private boolean useMockedLocation() {
+        return "false".equals(System.getProperty(PROPERTY_USE_MOCKED_LOCATION, "false"));
     }
 }