Merge "Update the CarLocationService to work when the system user is headless." into pi-dev
diff --git a/service/src/com/android/car/CarLocationService.java b/service/src/com/android/car/CarLocationService.java
index 6d93b40..a7aaf03 100644
--- a/service/src/com/android/car/CarLocationService.java
+++ b/service/src/com/android/car/CarLocationService.java
@@ -19,6 +19,7 @@
 import android.car.hardware.CarPropertyValue;
 import android.car.hardware.property.CarPropertyEvent;
 import android.car.hardware.property.ICarPropertyEventListener;
+import android.car.user.CarUserManagerHelper;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -31,6 +32,7 @@
 import android.os.HandlerThread;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.AtomicFile;
 import android.util.JsonReader;
 import android.util.JsonWriter;
@@ -60,6 +62,8 @@
     private static final long GRANULARITY_ONE_DAY_MS = 24 * 60 * 60 * 1000L;
     // The time-to-live for the cached location
     private static final long TTL_THIRTY_DAYS_MS = 30 * GRANULARITY_ONE_DAY_MS;
+    // The maximum number of times to try injecting a location
+    private static final int MAX_LOCATION_INJECTION_ATTEMPTS = 10;
 
     // Used internally for mHandlerThread synchronization
     private final Object mLock = new Object();
@@ -68,23 +72,26 @@
     private final CarPowerManagementService mCarPowerManagementService;
     private final CarPropertyService mCarPropertyService;
     private final CarPropertyEventListener mCarPropertyEventListener;
+    private final CarUserManagerHelper mCarUserManagerHelper;
     private int mTaskCount = 0;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
 
     public CarLocationService(Context context, CarPowerManagementService carPowerManagementService,
-            CarPropertyService carPropertyService) {
+            CarPropertyService carPropertyService, CarUserManagerHelper carUserManagerHelper) {
         logd("constructed");
         mContext = context;
         mCarPowerManagementService = carPowerManagementService;
         mCarPropertyService = carPropertyService;
         mCarPropertyEventListener = new CarPropertyEventListener();
+        mCarUserManagerHelper = carUserManagerHelper;
     }
 
     @Override
     public void init() {
         logd("init");
         IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
         filter.addAction(Intent.ACTION_LOCKED_BOOT_COMPLETED);
         filter.addAction(LocationManager.MODE_CHANGED_ACTION);
         filter.addAction(LocationManager.GPS_ENABLED_CHANGE_ACTION);
@@ -107,17 +114,19 @@
         writer.println(TAG);
         writer.println("Context: " + mContext);
         writer.println("CarPropertyService: " + mCarPropertyService);
+        writer.println("MAX_LOCATION_INJECTION_ATTEMPTS: " + MAX_LOCATION_INJECTION_ATTEMPTS);
     }
 
     @Override
     public long onPrepareShutdown(boolean shuttingDown) {
         logd("onPrepareShutdown " + shuttingDown);
         asyncOperation(() -> storeLocation());
-        return 0;
+        return 100;
     }
 
     @Override
-    public void onPowerOn(boolean displayOn) { }
+    public void onPowerOn(boolean displayOn) {
+    }
 
     @Override
     public int getWakeupTime() {
@@ -129,27 +138,55 @@
         logd("onReceive " + intent);
         String action = intent.getAction();
         if (action == Intent.ACTION_LOCKED_BOOT_COMPLETED) {
-            asyncOperation(() -> loadLocation());
-        } else {
+            // If the system user is not headless, then we can inject location as soon as the
+            // system has completed booting.
+            if (!mCarUserManagerHelper.isHeadlessSystemUser()) {
+                logd("not headless on boot complete");
+                asyncOperation(() -> loadLocation());
+            }
+        } else if (action == Intent.ACTION_USER_SWITCHED) {
+            int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+            logd("USER_SWITCHED: " + userHandle);
+            if (mCarUserManagerHelper.isHeadlessSystemUser()
+                    && userHandle > UserHandle.USER_SYSTEM) {
+                asyncOperation(() -> loadLocation());
+            }
+        } else if (action == LocationManager.MODE_CHANGED_ACTION
+                && shouldCheckLocationPermissions()) {
             LocationManager locationManager =
                     (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
-            if (action == LocationManager.MODE_CHANGED_ACTION) {
-                boolean locationEnabled = locationManager.isLocationEnabled();
-                logd("isLocationEnabled(): " + locationEnabled);
-                if (!locationEnabled) {
-                    asyncOperation(() -> deleteCacheFile());
-                }
-            } else if (action == LocationManager.GPS_ENABLED_CHANGE_ACTION) {
-                boolean gpsEnabled =
-                        locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
-                logd("isProviderEnabled('gps'): " + gpsEnabled);
-                if (!gpsEnabled) {
-                    asyncOperation(() -> deleteCacheFile());
-                }
+            boolean locationEnabled = locationManager.isLocationEnabled();
+            logd("isLocationEnabled(): " + locationEnabled);
+            if (!locationEnabled) {
+                asyncOperation(() -> deleteCacheFile());
+            }
+        } else if (action == LocationManager.GPS_ENABLED_CHANGE_ACTION
+                && shouldCheckLocationPermissions()) {
+            LocationManager locationManager =
+                    (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+            boolean gpsEnabled =
+                    locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+            logd("isProviderEnabled('gps'): " + gpsEnabled);
+            if (!gpsEnabled) {
+                asyncOperation(() -> deleteCacheFile());
             }
         }
     }
 
+    /**
+     * Tells whether or not we should check location permissions for the sake of deleting the
+     * location cache file when permissions are lacking.  If the system user is headless but the
+     * current user is still the system user, then we should not respond to a lack of location
+     * permissions.
+     */
+    private boolean shouldCheckLocationPermissions() {
+        return !(mCarUserManagerHelper.isHeadlessSystemUser()
+                && mCarUserManagerHelper.isCurrentProcessSystemUser());
+    }
+
+    /**
+     * Gets the last known location from the LocationManager and store it in a file.
+     */
     private void storeLocation() {
         LocationManager locationManager =
                 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
@@ -163,44 +200,44 @@
             FileOutputStream fos = null;
             try {
                 fos = atomicFile.startWrite();
-                JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(fos, "UTF-8"));
-                jsonWriter.beginObject();
-                jsonWriter.name("provider").value(location.getProvider());
-                jsonWriter.name("latitude").value(location.getLatitude());
-                jsonWriter.name("longitude").value(location.getLongitude());
-                if (location.hasAltitude()) {
-                    jsonWriter.name("altitude").value(location.getAltitude());
+                try (JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(fos, "UTF-8"))) {
+                    jsonWriter.beginObject();
+                    jsonWriter.name("provider").value(location.getProvider());
+                    jsonWriter.name("latitude").value(location.getLatitude());
+                    jsonWriter.name("longitude").value(location.getLongitude());
+                    if (location.hasAltitude()) {
+                        jsonWriter.name("altitude").value(location.getAltitude());
+                    }
+                    if (location.hasSpeed()) {
+                        jsonWriter.name("speed").value(location.getSpeed());
+                    }
+                    if (location.hasBearing()) {
+                        jsonWriter.name("bearing").value(location.getBearing());
+                    }
+                    if (location.hasAccuracy()) {
+                        jsonWriter.name("accuracy").value(location.getAccuracy());
+                    }
+                    if (location.hasVerticalAccuracy()) {
+                        jsonWriter.name("verticalAccuracy").value(
+                                location.getVerticalAccuracyMeters());
+                    }
+                    if (location.hasSpeedAccuracy()) {
+                        jsonWriter.name("speedAccuracy").value(
+                                location.getSpeedAccuracyMetersPerSecond());
+                    }
+                    if (location.hasBearingAccuracy()) {
+                        jsonWriter.name("bearingAccuracy").value(
+                                location.getBearingAccuracyDegrees());
+                    }
+                    if (location.isFromMockProvider()) {
+                        jsonWriter.name("isFromMockProvider").value(true);
+                    }
+                    long currentTime = location.getTime();
+                    // Round the time down to only be accurate within one day.
+                    jsonWriter.name("captureTime").value(
+                            currentTime - currentTime % GRANULARITY_ONE_DAY_MS);
+                    jsonWriter.endObject();
                 }
-                if (location.hasSpeed()) {
-                    jsonWriter.name("speed").value(location.getSpeed());
-                }
-                if (location.hasBearing()) {
-                    jsonWriter.name("bearing").value(location.getBearing());
-                }
-                if (location.hasAccuracy()) {
-                    jsonWriter.name("accuracy").value(location.getAccuracy());
-                }
-                if (location.hasVerticalAccuracy()) {
-                    jsonWriter.name("verticalAccuracy").value(
-                            location.getVerticalAccuracyMeters());
-                }
-                if (location.hasSpeedAccuracy()) {
-                    jsonWriter.name("speedAccuracy").value(
-                            location.getSpeedAccuracyMetersPerSecond());
-                }
-                if (location.hasBearingAccuracy()) {
-                    jsonWriter.name("bearingAccuracy").value(
-                            location.getBearingAccuracyDegrees());
-                }
-                if (location.isFromMockProvider()) {
-                    jsonWriter.name("isFromMockProvider").value(true);
-                }
-                long currentTime = location.getTime();
-                // Round the time down to only be accurate within one day.
-                jsonWriter.name("captureTime").value(
-                        currentTime - currentTime % GRANULARITY_ONE_DAY_MS);
-                jsonWriter.endObject();
-                jsonWriter.close();
                 atomicFile.finishWrite(fos);
             } catch (IOException e) {
                 Log.e(TAG, "Unable to write to disk", e);
@@ -209,6 +246,9 @@
         }
     }
 
+    /**
+     * Reads a previously stored location and attempts to inject it into the LocationManager.
+     */
     private void loadLocation() {
         Location location = readLocationFromCacheFile();
         logd("Read location from " + location.getTime());
@@ -220,10 +260,7 @@
             long elapsedTime = SystemClock.elapsedRealtimeNanos();
             location.setElapsedRealtimeNanos(elapsedTime);
             if (location.isComplete()) {
-                LocationManager locationManager =
-                        (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
-                boolean success = locationManager.injectLocation(location);
-                logd("Injected location " + location + " with result " + success);
+                injectLocation(location, 1);
             }
         }
     }
@@ -231,8 +268,7 @@
     private Location readLocationFromCacheFile() {
         Location location = new Location((String) null);
         AtomicFile atomicFile = new AtomicFile(mContext.getFileStreamPath(FILENAME));
-        try {
-            FileInputStream fis = atomicFile.openRead();
+        try (FileInputStream fis = atomicFile.openRead()) {
             JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8"));
             reader.beginObject();
             while (reader.hasNext()) {
@@ -266,7 +302,6 @@
                 }
             }
             reader.endObject();
-            fis.close();
             deleteCacheFile();
         } catch (FileNotFoundException e) {
             Log.d(TAG, "Location cache file not found.");
@@ -283,8 +318,33 @@
         mContext.deleteFile(FILENAME);
     }
 
+    /**
+     * Attempts to inject the location multiple times in case the LocationManager was not fully
+     * initialized or has not updated its handle to the current user yet.
+     */
+    private void injectLocation(Location location, int attemptCount) {
+        LocationManager locationManager =
+                (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+        boolean success = locationManager.injectLocation(location);
+        logd("Injected location " + location + " with result " + success + " on attempt "
+                + attemptCount);
+        if (success) {
+            return;
+        } else if (attemptCount <= MAX_LOCATION_INJECTION_ATTEMPTS) {
+            asyncOperation(() -> {
+                injectLocation(location, attemptCount + 1);
+            }, 200 * attemptCount);
+        } else {
+            logd("No location injected.");
+        }
+    }
+
     @VisibleForTesting
     void asyncOperation(Runnable operation) {
+        asyncOperation(operation, 0);
+    }
+
+    private void asyncOperation(Runnable operation, long delayMillis) {
         synchronized (mLock) {
             // Create a new HandlerThread if this is the first task to queue.
             if (++mTaskCount == 1) {
@@ -293,7 +353,7 @@
                 mHandler = new Handler(mHandlerThread.getLooper());
             }
         }
-        mHandler.post(() -> {
+        mHandler.postDelayed(() -> {
             try {
                 operation.run();
             } finally {
@@ -306,7 +366,7 @@
                     }
                 }
             }
-        });
+        }, delayMillis);
     }
 
     private static void logd(String msg) {
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 9a15631..67c50a7 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -123,8 +123,6 @@
         mCarInputService = new CarInputService(serviceContext, mHal.getInputHal());
         mCarProjectionService = new CarProjectionService(serviceContext, mCarInputService);
         mGarageModeService = new GarageModeService(mContext, mCarPowerManagementService);
-        mCarLocationService = new CarLocationService(mContext, mCarPowerManagementService,
-                mCarPropertyService);
         mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
         mCarAudioService = new CarAudioService(serviceContext);
         mCarNightService = new CarNightService(serviceContext, mCarPropertyService);
@@ -143,6 +141,8 @@
         mCarConfigurationService =
                 new CarConfigurationService(serviceContext, new JsonReaderImpl());
         mUserManagerHelper = new CarUserManagerHelper(serviceContext);
+        mCarLocationService = new CarLocationService(mContext, mCarPowerManagementService,
+                mCarPropertyService, mUserManagerHelper);
 
         // Be careful with order. Service depending on other service should be inited later.
         List<CarServiceBase> allServices = new ArrayList<>();
@@ -153,7 +153,6 @@
         allServices.add(mCarUXRestrictionsService);
         allServices.add(mCarPackageManagerService);
         allServices.add(mCarInputService);
-        allServices.add(mCarLocationService);
         allServices.add(mGarageModeService);
         allServices.add(mAppFocusService);
         allServices.add(mCarAudioService);
@@ -174,6 +173,7 @@
             allServices.add(mCarUserService);
         }
 
+        allServices.add(mCarLocationService);
         mAllServices = allServices.toArray(new CarServiceBase[allServices.size()]);
     }
 
diff --git a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
index 8ac7d9b..b071e99 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
@@ -31,6 +31,7 @@
 import android.car.hardware.CarSensorManager;
 import android.car.hardware.property.CarPropertyEvent;
 import android.car.hardware.property.ICarPropertyEventListener;
+import android.car.user.CarUserManagerHelper;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -72,7 +73,9 @@
  * The following mocks are used:
  * 1. {@link Context} provides files and a mocked {@link LocationManager}.
  * 2. {@link LocationManager} provides dummy {@link Location}s.
- * 3. {@link CarSensorService} registers a handler for sensor events and sends ignition-off events.
+ * 3. {@link CarPropertyService} registers a listener for ignition state events.
+ * 3. {@link CarPowerManagementService} registers a handler for power events.
+ * 4. {@link CarUserManagerHelper} tells whether or not the system user is headless.
  */
 @RunWith(AndroidJUnit4.class)
 public class CarLocationServiceTest {
@@ -85,6 +88,7 @@
     @Mock private LocationManager mMockLocationManager;
     @Mock private CarPropertyService mMockCarPropertyService;
     @Mock private CarPowerManagementService mMockCarPowerManagementService;
+    @Mock private CarUserManagerHelper mMockCarUserManagerHelper;
 
     /**
      * Initialize all of the objects with the @Mock annotation.
@@ -95,7 +99,7 @@
         mContext = InstrumentationRegistry.getTargetContext();
         mLatch = new CountDownLatch(1);
         mCarLocationService = new CarLocationService(mMockContext, mMockCarPowerManagementService,
-                mMockCarPropertyService) {
+                mMockCarPropertyService, mMockCarUserManagerHelper) {
             @Override
             void asyncOperation(Runnable operation) {
                 super.asyncOperation(() -> {
@@ -143,12 +147,13 @@
                 mCarLocationService);
         verify(mMockContext).registerReceiver(eq(mCarLocationService), argument.capture());
         IntentFilter intentFilter = argument.getValue();
-        assertEquals(3, intentFilter.countActions());
+        assertEquals(4, intentFilter.countActions());
         String[] actions = {intentFilter.getAction(0), intentFilter.getAction(1),
-                intentFilter.getAction(2)};
+                intentFilter.getAction(2), intentFilter.getAction(3)};
         assertTrue(ArrayUtils.contains(actions, Intent.ACTION_LOCKED_BOOT_COMPLETED));
         assertTrue(ArrayUtils.contains(actions, LocationManager.MODE_CHANGED_ACTION));
         assertTrue(ArrayUtils.contains(actions, LocationManager.GPS_ENABLED_CHANGE_ACTION));
+        assertTrue(ArrayUtils.contains(actions, Intent.ACTION_USER_SWITCHED));
         verify(mMockCarPropertyService).registerListener(
                 eq(CarSensorManager.SENSOR_TYPE_IGNITION_STATE), eq(0.0f), any());
     }
@@ -166,10 +171,11 @@
 
     /**
      * Test that the {@link CarLocationService} parses a location from a JSON serialization and then
-     * injects it into the {@link LocationManager} upon boot complete.
+     * injects it into the {@link LocationManager} upon boot complete if the system user is not
+     * headless.
      */
     @Test
-    public void testLoadsLocation() throws IOException, InterruptedException {
+    public void testLoadsLocationOnLockedBootComplete() throws IOException, InterruptedException {
         long currentTime = System.currentTimeMillis();
         long elapsedTime = SystemClock.elapsedRealtimeNanos();
         long pastTime = currentTime - 60000;
@@ -181,6 +187,7 @@
         when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true);
         when(mMockContext.getFileStreamPath("location_cache.json"))
                 .thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
+        when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(false);
 
         mCarLocationService.onReceive(mMockContext,
                 new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
@@ -196,6 +203,39 @@
     }
 
     /**
+     * Test that the {@link CarLocationService} parses a location from a JSON seialization and then
+     * injects it into the {@link LocationManager} upon user switch if the system user is headless.
+     */
+    @Test
+    public void testLoadsLocationWithHeadlessSystemUser() throws IOException, InterruptedException {
+        long currentTime = System.currentTimeMillis();
+        long elapsedTime = SystemClock.elapsedRealtimeNanos();
+        long pastTime = currentTime - 60000;
+        writeCacheFile("{\"provider\": \"gps\", \"latitude\": 16.7666, \"longitude\": 3.0026,"
+                + "\"accuracy\":12.3, \"captureTime\": " + pastTime + "}");
+        ArgumentCaptor<Location> argument = ArgumentCaptor.forClass(Location.class);
+        when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
+                .thenReturn(mMockLocationManager);
+        when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true);
+        when(mMockContext.getFileStreamPath("location_cache.json"))
+                .thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
+        when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(true);
+
+        Intent userSwitchedIntent = new Intent(Intent.ACTION_USER_SWITCHED);
+        userSwitchedIntent.putExtra(Intent.EXTRA_USER_HANDLE, 11);
+        mCarLocationService.onReceive(mMockContext, userSwitchedIntent);
+        mLatch.await();
+
+        Location location = argument.getValue();
+        assertEquals("gps", location.getProvider());
+        assertEquals(16.7666, location.getLatitude());
+        assertEquals(3.0026, location.getLongitude());
+        assertEquals(12.3f, location.getAccuracy());
+        assertTrue(location.getTime() >= currentTime);
+        assertTrue(location.getElapsedRealtimeNanos() >= elapsedTime);
+    }
+
+    /**
      * Test that the {@link CarLocationService} does not inject a location if there is no location
      * cache file.
      */