Merge "Add a dummy launcher for CTS" into qt-dev
diff --git a/service/src/com/android/car/CarLocationService.java b/service/src/com/android/car/CarLocationService.java
index ad86741..43d4a46 100644
--- a/service/src/com/android/car/CarLocationService.java
+++ b/service/src/com/android/car/CarLocationService.java
@@ -125,6 +125,11 @@
                     }
                 });
                 break;
+            case CarPowerStateListener.SUSPEND_EXIT:
+                deleteCacheFile();
+                if (future != null) {
+                    future.complete(null);
+                }
             default:
                 // This service does not need to do any work for these events but should still
                 // notify the CarPowerManager that it may proceed.
@@ -153,7 +158,7 @@
             boolean locationEnabled = locationManager.isLocationEnabled();
             logd("isLocationEnabled(): " + locationEnabled);
             if (!locationEnabled) {
-                asyncOperation(() -> deleteCacheFile());
+                deleteCacheFile();
             }
         } else if (action == LocationManager.PROVIDERS_CHANGED_ACTION
                 && shouldCheckLocationPermissions()) {
@@ -163,7 +168,7 @@
                     locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
             logd("isProviderEnabled('gps'): " + gpsEnabled);
             if (!gpsEnabled) {
-                asyncOperation(() -> deleteCacheFile());
+                deleteCacheFile();
             }
         }
     }
@@ -188,7 +193,6 @@
         Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
         if (location == null) {
             logd("Not storing null location");
-            deleteCacheFile();
         } else {
             logd("Storing location: " + location);
             AtomicFile atomicFile = new AtomicFile(getLocationCacheFile());
@@ -250,6 +254,7 @@
         long currentTime = System.currentTimeMillis();
         if (location.getTime() + TTL_THIRTY_DAYS_MS < currentTime) {
             logd("Location expired.");
+            deleteCacheFile();
         } else {
             location.setTime(currentTime);
             long elapsedTime = SystemClock.elapsedRealtimeNanos();
@@ -297,7 +302,6 @@
                 }
             }
             reader.endObject();
-            deleteCacheFile();
         } catch (FileNotFoundException e) {
             Log.d(TAG, "Location cache file not found.");
         } catch (IOException e) {
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index bac93f9..5728bf2 100644
--- a/service/src/com/android/car/CarMediaService.java
+++ b/service/src/com/android/car/CarMediaService.java
@@ -477,7 +477,9 @@
             // e.g. Assistant starts playback, user uses hardware button, etc.
             mStartPlayback = false;
         }
-        mSharedPrefs.edit().putInt(PLAYBACK_STATE_KEY, state).apply();
+        if (mSharedPrefs != null) {
+            mSharedPrefs.edit().putInt(PLAYBACK_STATE_KEY, state).apply();
+        }
     }
 
     private void maybeRestartPlayback(PlaybackState state) {
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 2f90d3d..244ee8b 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -449,7 +449,7 @@
                     + " without permission " + android.Manifest.permission.DUMP);
             return;
         }
-        if (args == null || args.length == 0) {
+        if (args == null || args.length == 0 || (args.length > 0 && "-a".equals(args[0]))) {
             writer.println("*dump car service*");
 
             writer.println("*FutureConfig, DEFAULT:" + FeatureConfiguration.DEFAULT);
@@ -702,19 +702,19 @@
             switch (arg) {
                 case PARAM_ON_MODE:
                     mGarageModeService.forceStartGarageMode();
+                    writer.println("Garage mode: " + mGarageModeService.isGarageModeActive());
                     break;
                 case PARAM_OFF_MODE:
                     mGarageModeService.stopAndResetGarageMode();
+                    writer.println("Garage mode: " + mGarageModeService.isGarageModeActive());
                     break;
                 case PARAM_QUERY_MODE:
-                    // Nothing to do. Always query at the end anyway.
+                    mGarageModeService.dump(writer);
                     break;
                 default:
                     writer.println("Unknown value. Valid argument: " + PARAM_ON_MODE + "|"
                             + PARAM_OFF_MODE + "|" + PARAM_QUERY_MODE);
-                    return;
             }
-            writer.println("Garage mode: " + mGarageModeService.isGarageModeActive());
         }
 
         /**
diff --git a/service/src/com/android/car/garagemode/Controller.java b/service/src/com/android/car/garagemode/Controller.java
index 347ba73..d7235dd 100644
--- a/service/src/com/android/car/garagemode/Controller.java
+++ b/service/src/com/android/car/garagemode/Controller.java
@@ -28,6 +28,7 @@
 import com.android.car.CarLocalServices;
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -105,6 +106,13 @@
     }
 
     /**
+     * @return The names of the jobs that Garage Mode is waiting for
+     */
+    List<String> pendingGarageModeJobs() {
+        return mGarageMode.pendingJobs();
+    }
+
+    /**
      * Wrapper method to send a broadcast
      *
      * @param i intent that contains broadcast data
diff --git a/service/src/com/android/car/garagemode/GarageMode.java b/service/src/com/android/car/garagemode/GarageMode.java
index b13045e..1165de5 100644
--- a/service/src/com/android/car/garagemode/GarageMode.java
+++ b/service/src/com/android/car/garagemode/GarageMode.java
@@ -63,6 +63,7 @@
 
     private boolean mGarageModeActive;
     private JobScheduler mJobScheduler;
+    private List<String> mPendingJobs = new ArrayList<>();
     private Handler mHandler;
     private Runnable mRunnable = new Runnable() {
         @Override
@@ -129,6 +130,10 @@
         return mGarageModeActive;
     }
 
+    synchronized List<String> pendingJobs() {
+        return mPendingJobs;
+    }
+
     void enterGarageMode(CompletableFuture<Void> future) {
         LOG.d("Entering GarageMode");
         synchronized (this) {
@@ -141,9 +146,7 @@
         ArrayList<Integer> startedUsers =
                 CarLocalServices.getService(CarUserService.class).startAllBackgroundUsers();
         synchronized (this) {
-            for (Integer user : startedUsers) {
-                mStartedBackgroundUsers.add(user);
-            }
+            mStartedBackgroundUsers.addAll(startedUsers);
         }
     }
 
@@ -224,15 +227,22 @@
         mHandler.removeCallbacks(mRunnable);
     }
 
-    private int numberOfJobsRunning() {
+    private synchronized int numberOfJobsRunning() {
         List<JobInfo> startedJobs = mJobScheduler.getStartedJobs();
         int count = 0;
+        List<String> currentPendingJobs = new ArrayList<>();
         for (JobSnapshot snap : mJobScheduler.getAllJobSnapshots()) {
             if (startedJobs.contains(snap.getJobInfo())
                     && snap.getJobInfo().isRequireDeviceIdle()) {
+                currentPendingJobs.add(snap.getJobInfo().toString());
                 count++;
             }
         }
+        if (count > 0) {
+            // We have something pending, so update the list.
+            // (Otherwise, keep the old list.)
+            mPendingJobs = currentPendingJobs;
+        }
         return count;
     }
 }
diff --git a/service/src/com/android/car/garagemode/GarageModeService.java b/service/src/com/android/car/garagemode/GarageModeService.java
index f28bb35..8527bc7 100644
--- a/service/src/com/android/car/garagemode/GarageModeService.java
+++ b/service/src/com/android/car/garagemode/GarageModeService.java
@@ -23,6 +23,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
+import java.util.List;
 
 /**
  * Main service container for car Garage Mode.
@@ -63,11 +64,24 @@
 
     /**
      * Dumps useful information about GarageMode
-     * @param writer
+     * @param writer Where to dump the information
      */
     @Override
     public void dump(PrintWriter writer) {
-        writer.println("GarageModeInProgress " + mController.isGarageModeActive());
+        boolean isActive = mController.isGarageModeActive();
+        writer.println("GarageModeInProgress " + isActive);
+        List<String> jobs = mController.pendingGarageModeJobs();
+        if (isActive) {
+            writer.println("GarageMode is currently waiting for " + jobs.size() + " jobs:");
+        } else {
+            writer.println("GarageMode was last waiting for " + jobs.size() + " jobs:");
+        }
+        // Dump the names of the jobs that GM is/was waiting for
+        int jobNumber = 1;
+        for (String job : jobs) {
+            writer.println("   " + jobNumber + ": " + job);
+            jobNumber++;
+        }
     }
 
     /**
diff --git a/service/src/com/android/car/hal/InputHalService.java b/service/src/com/android/car/hal/InputHalService.java
index f0993eb..d719510 100644
--- a/service/src/com/android/car/hal/InputHalService.java
+++ b/service/src/com/android/car/hal/InputHalService.java
@@ -140,12 +140,19 @@
                             KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
             int code = v.value.int32Values.get(1);
             int display = v.value.int32Values.get(2);
+            int indentsCount = v.value.int32Values.size() < 4 ? 1 : v.value.int32Values.get(3);
             if (DBG) {
-                Log.i(CarLog.TAG_INPUT, "hal event code:" + code + ", action:" + action +
-                        ", display:" + display);
+                Log.i(CarLog.TAG_INPUT, new StringBuilder()
+                                        .append("hal event code:").append(code)
+                                        .append(", action:").append(action)
+                                        .append(", display: ").append(display)
+                                        .append(", number of indents: ").append(indentsCount)
+                                        .toString());
             }
-
-            dispatchKeyEvent(listener, action, code, display);
+            while (indentsCount > 0) {
+                indentsCount--;
+                dispatchKeyEvent(listener, action, code, display);
+            }
         }
     }
 
diff --git a/service/src/com/android/car/trust/BleManager.java b/service/src/com/android/car/trust/BleManager.java
index fd319d6..24a3b79 100644
--- a/service/src/com/android/car/trust/BleManager.java
+++ b/service/src/com/android/car/trust/BleManager.java
@@ -225,6 +225,9 @@
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "stopGattServer");
         }
+        if (mBluetoothGatt != null) {
+            mBluetoothGatt.disconnect();
+        }
         mGattServer.close();
         mGattServer = null;
     }
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index cb0f908..6743375 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -531,7 +531,7 @@
                 notifyEscrowTokenReceived(value);
                 break;
             case ENROLLMENT_STATE_HANDLE:
-                // TODO(danharms): React to ACK. Shut down server?
+                mCarTrustAgentBleManager.disconnectRemoteDevice();
                 break;
             default:
                 // Should never get here
diff --git a/tests/carservice_unit_test/Android.mk b/tests/carservice_unit_test/Android.mk
index 3267fa6..6b715fc 100644
--- a/tests/carservice_unit_test/Android.mk
+++ b/tests/carservice_unit_test/Android.mk
@@ -57,8 +57,8 @@
     frameworks-base-testutils \
     truth-prebuilt
 
+# mockito-target-inline dependency
 LOCAL_JNI_SHARED_LIBRARIES := \
-    # mockito-target-inline dependency \
     libdexmakerjvmtiagent \
 
 include $(BUILD_PACKAGE)
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 f653709..4290354 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
@@ -292,10 +292,10 @@
 
     /**
      * Test that the {@link CarLocationService} stores the {@link LocationManager}'s last known
-     * location upon power state-changed SUSPEND events.
+     * location upon power state-changed SHUTDOWN_PREPARE events.
      */
     @Test
-    public void testStoresLocationUponStateChanged() throws Exception {
+    public void testStoresLocationUponShutdownPrepare() throws Exception {
         long currentTime = System.currentTimeMillis();
         long elapsedTime = SystemClock.elapsedRealtimeNanos();
         Location timbuktu = new Location(LocationManager.GPS_PROVIDER);
@@ -367,12 +367,28 @@
         mCarLocationService.init();
         mCarLocationService.onReceive(mMockContext,
                 new Intent(LocationManager.MODE_CHANGED_ACTION));
-        mLatch.await();
         verify(mMockLocationManager, times(1)).isLocationEnabled();
         assertFalse(getLocationCacheFile().exists());
     }
 
     /**
+     * Test that the {@link CarLocationService} deletes location_cache.json when the system resumes
+     * from suspend-to-ram.
+     */
+    @Test
+    public void testDeletesCacheFileUponSuspendExit() throws Exception {
+        when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
+                .thenReturn(mMockLocationManager);
+        when(mMockLocationManager.isLocationEnabled()).thenReturn(false);
+        mCarLocationService.init();
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        mCarLocationService.onStateChanged(CarPowerStateListener.SUSPEND_EXIT, future);
+        assertTrue(future.isDone());
+        verify(mMockLocationManager, times(0)).isLocationEnabled();
+        assertFalse(getLocationCacheFile().exists());
+    }
+
+    /**
      * Test that the {@link CarLocationService} deletes location_cache.json when the GPS location
      * provider is disabled.
      */
@@ -385,7 +401,6 @@
         mCarLocationService.init();
         mCarLocationService.onReceive(mMockContext,
                 new Intent(LocationManager.PROVIDERS_CHANGED_ACTION));
-        mLatch.await();
         verify(mMockLocationManager, times(1))
                 .isProviderEnabled(LocationManager.GPS_PROVIDER);
         assertFalse(getLocationCacheFile().exists());
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
index 32f8e1d..5cce922 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
@@ -181,6 +181,28 @@
         assertThat(event.getRepeatCount()).isEqualTo(0);
     }
 
+    /**
+     * Test for handling rotary knob event.
+     */
+    @Test
+    public void handlesRepeatedKeyWithIndents() {
+        subscribeListener();
+        KeyEvent event = dispatchSingleEventWithIndents(KeyEvent.KEYCODE_VOLUME_UP, 5);
+        assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
+        assertThat(event.getEventTime()).isEqualTo(0L);
+        assertThat(event.getDownTime()).isEqualTo(0L);
+        assertThat(event.getRepeatCount()).isEqualTo(4);
+
+        when(mUptimeSupplier.getAsLong()).thenReturn(5L);
+        event = dispatchSingleEventWithIndents(KeyEvent.KEYCODE_VOLUME_UP, 5);
+        assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
+        assertThat(event.getEventTime()).isEqualTo(5L);
+        assertThat(event.getDownTime()).isEqualTo(5L);
+        assertThat(event.getRepeatCount()).isEqualTo(9);
+    }
+
     @Test
     public void handlesKeyUp_withoutKeyDown() {
         subscribeListener();
@@ -237,4 +259,25 @@
         reset(mInputListener);
         return captor.getValue();
     }
-}
+
+    private VehiclePropValue makeKeyPropValueWithIndents(int code, int indents) {
+        VehiclePropValue v = new VehiclePropValue();
+        v.prop = VehicleProperty.HW_KEY_INPUT;
+        // Only Key.down can have indents.
+        v.value.int32Values.add(VehicleHwKeyInputAction.ACTION_DOWN);
+        v.value.int32Values.add(code);
+        v.value.int32Values.add(DISPLAY);
+        v.value.int32Values.add(indents);
+        return v;
+    }
+
+    private KeyEvent dispatchSingleEventWithIndents(int code, int indents) {
+        ArgumentCaptor<KeyEvent> captor = ArgumentCaptor.forClass(KeyEvent.class);
+        reset(mInputListener);
+        mInputHalService.handleHalEvents(
+                ImmutableList.of(makeKeyPropValueWithIndents(code, indents)));
+        verify(mInputListener).onKeyEvent(captor.capture(), eq(DISPLAY));
+        reset(mInputListener);
+        return captor.getValue();
+    }
+}
\ No newline at end of file