Start MediaCenter service for media source binding am: 92aa43ac6f am: 06a7a0ad0f
am: 86219c0abe

Change-Id: Ib5b53caa577a809bcf619330faad54f12eceba73
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 104d5d6..7c03328 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -53,9 +53,9 @@
     persist.bluetooth.enablenewavrcp=false \
     ro.carrier=unknown
 
-# Enable headless user 0
 PRODUCT_SYSTEM_DEFAULT_PROPERTIES += \
     ro.fw.mu.headless_system_user=true \
+    config.disable_systemtextclassifier=true
 
 # Overlay for Google network and fused location providers
 $(call inherit-product, device/sample/products/location_overlay.mk)
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index 62ff41c..b9d6179 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -58,6 +58,9 @@
     <!-- Automotive Bluetooth pairing option -->
     <bool name="enable_pbap_pce_profile">true</bool>
 
+    <!-- Whether the device supports quick settings and its associated APIs -->
+    <bool name="config_quickSettingsSupported">false</bool>
+
     <!-- Flag indicating that the entire notification header can be clicked to expand the
          notification. If false, then the expand icon has to be clicked in order for the expand
          to occur. -->
diff --git a/car_product/sepolicy/private/carservice_app.te b/car_product/sepolicy/private/carservice_app.te
index 817aa5d..bc1d74c 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -19,6 +19,7 @@
 allow carservice_app {
     accessibility_service
     activity_service
+    activity_task_service
     audio_service
     audioserver_service
     autofill_service
@@ -49,6 +50,8 @@
 allow carservice_app system_car_data_file:dir create_dir_perms;
 allow carservice_app system_car_data_file:{ file lnk_file } create_file_perms;
 
+net_domain(carservice_app)
+
 allow carservice_app cgroup:file rw_file_perms;
 
 # For I/O stats tracker
diff --git a/car_product/sepolicy/public/file.te b/car_product/sepolicy/public/file.te
index 8555d29..11bf839 100644
--- a/car_product/sepolicy/public/file.te
+++ b/car_product/sepolicy/public/file.te
@@ -5,4 +5,4 @@
 type sysfs_fs_lifetime_write, sysfs_type, fs_type;
 
 # /data/system/car
-type system_car_data_file, file_type, data_file_type, core_data_file_type;
\ No newline at end of file
+type system_car_data_file, file_type, data_file_type, core_data_file_type;
diff --git a/service/src/com/android/car/CarLocationService.java b/service/src/com/android/car/CarLocationService.java
index 3827c22..2f37e61 100644
--- a/service/src/com/android/car/CarLocationService.java
+++ b/service/src/com/android/car/CarLocationService.java
@@ -216,7 +216,17 @@
                 });
                 break;
             case CarPowerStateListener.SUSPEND_EXIT:
-                deleteCacheFile();
+                if (mCarDrivingStateService != null) {
+                    CarDrivingStateEvent event = mCarDrivingStateService.getCurrentDrivingState();
+                    if (event != null
+                            && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+                        deleteCacheFile();
+                    } else {
+                        logd("Registering to receive driving state.");
+                        mCarDrivingStateService.registerDrivingStateChangeListener(
+                                mICarDrivingStateChangeEventListener);
+                    }
+                }
                 if (future != null) {
                     future.complete(null);
                 }
diff --git a/service/src/com/android/car/CarUxRestrictionsManagerService.java b/service/src/com/android/car/CarUxRestrictionsManagerService.java
index 8b5e9e1..d85ebaa 100644
--- a/service/src/com/android/car/CarUxRestrictionsManagerService.java
+++ b/service/src/com/android/car/CarUxRestrictionsManagerService.java
@@ -60,6 +60,7 @@
 import com.android.car.systeminterface.SystemInterface;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -157,17 +158,17 @@
             mCurrentUxRestrictions.put(port, createUnrestrictedRestrictions());
         }
 
-        // subscribe to driving State
+        // Load the prod config, or if there is a staged one, promote that first only if the
+        // current driving state, as provided by the driving state service, is parked.
+        mCarUxRestrictionsConfigurations = convertToMap(loadConfig());
+
+        // subscribe to driving state changes
         mDrivingStateService.registerDrivingStateChangeListener(
                 mICarDrivingStateChangeEventListener);
         // subscribe to property service for speed
         mCarPropertyService.registerListener(VehicleProperty.PERF_VEHICLE_SPEED,
                 PROPERTY_UPDATE_RATE, mICarPropertyEventListener);
 
-        // At this point the driving state is known, which determines whether it's safe
-        // to promote staged new config.
-        mCarUxRestrictionsConfigurations = convertToMap(loadConfig());
-
         initializeUxRestrictions();
     }
 
@@ -187,7 +188,8 @@
      * <li>hardcoded default config.
      * </ol>
      *
-     * This method attempts to promote staged config file. Doing which depends on driving state.
+     * This method attempts to promote staged config file, which requires getting the current
+     * driving state.
      */
     @VisibleForTesting
     synchronized List<CarUxRestrictionsConfiguration> loadConfig() {
@@ -237,6 +239,10 @@
         return null;
     }
 
+    /**
+     * Promotes the staged config to prod, by replacing the prod file. Only do this if the car is
+     * parked to avoid changing the restrictions during a drive.
+     */
     private void promoteStagedConfig() {
         Path stagedConfig = getFile(CONFIG_FILENAME_STAGED).toPath();
 
@@ -699,6 +705,11 @@
      */
     private synchronized void handleDispatchUxRestrictions(@CarDrivingState int currentDrivingState,
             float speed) {
+        Preconditions.checkNotNull(mCarUxRestrictionsConfigurations,
+                "mCarUxRestrictionsConfigurations must be initialized");
+        Preconditions.checkNotNull(mCurrentUxRestrictions,
+                "mCurrentUxRestrictions must be initialized");
+
         if (isDebugBuild() && !mUxRChangeBroadcastEnabled) {
             Log.d(TAG, "Not dispatching UX Restriction due to setting");
             return;
diff --git a/service/src/com/android/car/garagemode/GarageMode.java b/service/src/com/android/car/garagemode/GarageMode.java
index 1165de5..4be06ff 100644
--- a/service/src/com/android/car/garagemode/GarageMode.java
+++ b/service/src/com/android/car/garagemode/GarageMode.java
@@ -231,17 +231,20 @@
         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++;
+        final List<JobSnapshot> allJobs = mJobScheduler.getAllJobSnapshots();
+        if (allJobs != null) {
+            for (JobSnapshot snap : allJobs) {
+                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;
+            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/trust/BLEMessageV1Factory.java b/service/src/com/android/car/trust/BLEMessageV1Factory.java
index afb5c8c..4afe877 100644
--- a/service/src/com/android/car/trust/BLEMessageV1Factory.java
+++ b/service/src/com/android/car/trust/BLEMessageV1Factory.java
@@ -41,14 +41,6 @@
      */
     private static final int FIXED_32_SIZE = 4;
 
-    /**
-     * Additional bytes that are needed during the encoding of the {@code payload} field.
-     *
-     * <p>The {@code payload} field is defined as {@code bytes}, and thus, needs 2 extra bytes to
-     * encode: one to encode the field number and another for length delimiting.
-     */
-    private static final int ADDITIONAL_PAYLOAD_ENCODING_SIZE = 2;
-
     // The size needed to encode a boolean proto field
     private static final int BOOLEAN_FIELD_ENCODING_SIZE = 1;
 
@@ -145,13 +137,14 @@
     public static List<BLEMessage> makeBLEMessages(byte[] payload, OperationType operation,
             int maxSize, boolean isPayloadEncrypted) {
         List<BLEMessage> bleMessages = new ArrayList();
-        int maxPayloadSize = maxSize - getProtoHeaderSize(operation, isPayloadEncrypted);
-        int payloadLength = payload.length;
-        if (payloadLength <= maxPayloadSize) {
+        int payloadSize = payload.length;
+        int maxPayloadSize =
+                maxSize - getProtoHeaderSize(operation, payloadSize, isPayloadEncrypted);
+        if (payloadSize <= maxPayloadSize) {
             bleMessages.add(makeBLEMessage(payload, operation, isPayloadEncrypted));
             return bleMessages;
         }
-        int totalPackets = (int) Math.ceil((double) payloadLength / maxPayloadSize);
+        int totalPackets = (int) Math.ceil((double) payloadSize / maxPayloadSize);
         int start = 0;
         int end = maxPayloadSize;
         for (int i = 0; i < totalPackets; i++) {
@@ -164,7 +157,7 @@
                     .setPayload(ByteString.copyFrom(Arrays.copyOfRange(payload, start, end)))
                     .build());
             start = end;
-            end = Math.min(start + maxPayloadSize, payloadLength);
+            end = Math.min(start + maxPayloadSize, payloadSize);
         }
         return bleMessages;
     }
@@ -174,12 +167,18 @@
      * contain a payload.
      */
     @VisibleForTesting
-    static int getProtoHeaderSize(OperationType operation, boolean isPayloadEncrypted) {
-        int isPayloadEncryptedFieldSize =
-                isPayloadEncrypted ? (BOOLEAN_FIELD_ENCODING_SIZE + FIELD_NUMBER_ENCODING_SIZE) : 0;
+    static int getProtoHeaderSize(OperationType operation, int payloadSize,
+            boolean isPayloadEncrypted) {
+        int isPayloadEncryptedFieldSize = isPayloadEncrypted
+                ? BOOLEAN_FIELD_ENCODING_SIZE + FIELD_NUMBER_ENCODING_SIZE
+                : 0;
         int operationSize = getEncodedSize(operation.getNumber()) + FIELD_NUMBER_ENCODING_SIZE;
+
+        // The payload size is a varint.
+        int payloadEncodingSize = FIELD_NUMBER_ENCODING_SIZE + getEncodedSize(payloadSize);
+
         return CONSTANT_HEADER_FIELD_SIZE + operationSize + isPayloadEncryptedFieldSize
-                + ADDITIONAL_PAYLOAD_ENCODING_SIZE;
+                + payloadEncodingSize;
     }
 
     /**
diff --git a/service/src/com/android/car/trust/BLEVersionExchangeResolver.java b/service/src/com/android/car/trust/BLEVersionExchangeResolver.java
index 7b9a025..4465b25 100644
--- a/service/src/com/android/car/trust/BLEVersionExchangeResolver.java
+++ b/service/src/com/android/car/trust/BLEVersionExchangeResolver.java
@@ -59,7 +59,7 @@
                 .setMinSupportedMessagingVersion(MESSAGING_VERSION)
                 .setMaxSupportedMessagingVersion(MESSAGING_VERSION)
                 .setMinSupportedSecurityVersion(SECURITY_VERSION)
-                .setMinSupportedSecurityVersion(SECURITY_VERSION)
+                .setMaxSupportedSecurityVersion(SECURITY_VERSION)
                 .build();
     }
 
diff --git a/service/src/com/android/car/trust/CarTrustAgentBleManager.java b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
index 09c59e6..1e43300 100644
--- a/service/src/com/android/car/trust/CarTrustAgentBleManager.java
+++ b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
@@ -57,7 +57,6 @@
  * the Trusted Device feature.
  */
 class CarTrustAgentBleManager extends BleManager {
-
     private static final String TAG = "CarTrustBLEManager";
 
     /**
@@ -70,6 +69,15 @@
     private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
             UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
 
+    /**
+     * Reserved bytes for an ATT write request payload.
+     *
+     * <p>The attribute protocol uses 3 bytes to encode the command type and attribute ID. These
+     * bytes need to be subtracted from the reported MTU size and the resulting value will
+     * represent the total amount of bytes that can be sent in a write.
+     */
+    private static final int ATT_PAYLOAD_RESERVED_BYTES = 3;
+
     /** @hide */
     @IntDef(prefix = {"TRUSTED_DEVICE_OPERATION_"}, value = {
             TRUSTED_DEVICE_OPERATION_NONE,
@@ -94,7 +102,16 @@
     private String mOriginalBluetoothName;
     private byte[] mUniqueId;
     private String mEnrollmentDeviceName;
-    private int mMtuSize = 20;
+
+    /**
+     * The maximum amount of bytes that can be written over BLE.
+     *
+     * <p>This initial value is 20 because BLE has a default write of 23 bytes. However, 3 bytes
+     * are subtracted due to bytes being reserved for the command type and attribute ID.
+     *
+     * @see #ATT_PAYLOAD_RESERVED_BYTES
+     */
+    private int mMaxWriteSize = 20;
 
     // Enrollment Service and Characteristic UUIDs
     private UUID mEnrollmentServiceUuid;
@@ -169,7 +186,12 @@
 
     @Override
     protected void onMtuSizeChanged(int size) {
-        mMtuSize = size;
+        mMaxWriteSize = size - ATT_PAYLOAD_RESERVED_BYTES;
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "MTU size changed to: " + size
+                    + "; setting max payload size to: " + mMaxWriteSize);
+        }
     }
 
     @Override
@@ -347,7 +369,8 @@
         // Characteristic the connected bluetooth device will write to.
         BluetoothGattCharacteristic clientCharacteristic =
                 new BluetoothGattCharacteristic(mEnrollmentClientWriteUuid,
-                        BluetoothGattCharacteristic.PROPERTY_WRITE,
+                        BluetoothGattCharacteristic.PROPERTY_WRITE
+                                | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
                         BluetoothGattCharacteristic.PERMISSION_WRITE);
 
         // Characteristic that this manager will write to.
@@ -380,7 +403,8 @@
         // Characteristic the connected bluetooth device will write to.
         BluetoothGattCharacteristic clientCharacteristic = new BluetoothGattCharacteristic(
                 mUnlockClientWriteUuid,
-                BluetoothGattCharacteristic.PROPERTY_WRITE,
+                BluetoothGattCharacteristic.PROPERTY_WRITE
+                        | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
                 BluetoothGattCharacteristic.PERMISSION_WRITE);
 
         // Characteristic that this manager will write to.
@@ -524,7 +548,7 @@
         }
 
         List<BLEMessage> bleMessages = BLEMessageV1Factory.makeBLEMessages(message, operation,
-                mMtuSize, isPayloadEncrypted);
+                mMaxWriteSize, isPayloadEncrypted);
 
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "sending " + bleMessages.size() + " messages to device");
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
index 24f66b9..ba92a46 100644
--- a/service/src/com/android/car/vms/VmsClientManager.java
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -359,6 +359,7 @@
         private final String mFullName;
         private boolean mIsBound = false;
         private boolean mIsTerminated = false;
+        private boolean mRebindScheduled = false;
         private IBinder mClientService;
 
         ClientConnection(ComponentName name, UserHandle user) {
@@ -400,33 +401,54 @@
                 Log.e(TAG, "While unbinding " + mFullName, t);
             }
             mIsBound = false;
-            if (mClientService != null) {
-                notifyListenersOnClientDisconnected(mFullName);
-            }
-            mClientService = null;
         }
 
-        synchronized void rebind() {
-            unbind();
+        synchronized void scheduleRebind() {
+            if (mRebindScheduled) {
+                return;
+            }
+
             if (DBG) {
                 Log.d(TAG,
                         String.format("rebinding %s after %dms", mFullName, mMillisBeforeRebind));
             }
-            if (!mIsTerminated) {
-                mHandler.postDelayed(this::bind, mMillisBeforeRebind);
-                synchronized (mRebindCounts) {
-                    mRebindCounts.computeIfAbsent(mName.getPackageName(), k -> new AtomicLong())
-                            .incrementAndGet();
-                }
+            mHandler.postDelayed(this::doRebind, mMillisBeforeRebind);
+            mRebindScheduled = true;
+        }
+
+        synchronized void doRebind() {
+            mRebindScheduled = false;
+            // Do not rebind if the connection has been terminated, or the client service has
+            // reconnected on its own.
+            if (mIsTerminated || mClientService != null) {
+                return;
+            }
+
+            if (DBG) Log.d(TAG, "rebinding: " + mFullName);
+            // Ensure that the client is not bound before attempting to rebind.
+            // If the client is not currently bound, unbind() will have no effect.
+            unbind();
+            bind();
+            synchronized (mRebindCounts) {
+                mRebindCounts.computeIfAbsent(mName.getPackageName(), k -> new AtomicLong())
+                        .incrementAndGet();
             }
         }
 
         synchronized void terminate() {
             if (DBG) Log.d(TAG, "terminating: " + mFullName);
             mIsTerminated = true;
+            notifyOnDisconnect();
             unbind();
         }
 
+        synchronized void notifyOnDisconnect() {
+            if (mClientService != null) {
+                notifyListenersOnClientDisconnected(mFullName);
+                mClientService = null;
+            }
+        }
+
         synchronized void notifyIfConnected(ConnectionListener listener) {
             if (mClientService != null) {
                 listener.onClientConnected(mFullName, mClientService);
@@ -443,7 +465,15 @@
         @Override
         public void onServiceDisconnected(ComponentName name) {
             if (DBG) Log.d(TAG, "onServiceDisconnected: " + mFullName);
-            rebind();
+            notifyOnDisconnect();
+            scheduleRebind();
+        }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            if (DBG) Log.d(TAG, "onBindingDied: " + mFullName);
+            notifyOnDisconnect();
+            scheduleRebind();
         }
 
         @Override
diff --git a/tests/GarageModeTestApp/res/layout/offcar_testing.xml b/tests/GarageModeTestApp/res/layout/offcar_testing.xml
index c14aa07..1b12752 100644
--- a/tests/GarageModeTestApp/res/layout/offcar_testing.xml
+++ b/tests/GarageModeTestApp/res/layout/offcar_testing.xml
@@ -90,22 +90,14 @@
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:layout_weight="1">
-                <LinearLayout
+                <CheckBox
+                    style="@style/Checkbox"
+                    android:id="@+id/requireIdlenessCheckbox"
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
-                    android:layout_weight="1">
-                    <TextView
-                        style="@style/SpinnerLabel"
-                        android:text="@string/spinner_label_network_type"/>
-                    <LinearLayout
-                        style="@style/SectionContainer"
-                        android:layout_width="wrap_content"
-                        android:layout_height="match_parent">
-                        <Spinner
-                            style="@style/Spinner"
-                            android:id="@+id/networkType"/>
-                    </LinearLayout>
-                </LinearLayout>
+                    android:layout_weight="1"
+                    android:checked="true"
+                    android:text="@string/checkbox_require_idleness"/>
                 <CheckBox
                     style="@style/Checkbox"
                     android:id="@+id/requirePersistedCheckbox"
@@ -121,13 +113,6 @@
                 android:weightSum="2">
                 <CheckBox
                     style="@style/Checkbox"
-                    android:id="@+id/requireIdlenessCheckbox"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:layout_weight="1"
-                    android:text="@string/checkbox_require_idleness"/>
-                <CheckBox
-                    style="@style/Checkbox"
                     android:id="@+id/requireChargingCheckbox"
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
@@ -137,6 +122,22 @@
             <LinearLayout
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
+                android:layout_weight="1">
+                <TextView
+                    style="@style/SpinnerLabel"
+                    android:text="@string/spinner_label_network_type"/>
+                <LinearLayout
+                    style="@style/SectionContainer"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent">
+                    <Spinner
+                        style="@style/Spinner"
+                        android:id="@+id/networkType"/>
+                </LinearLayout>
+            </LinearLayout>
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
                 android:layout_weight="1"
                 android:orientation="horizontal">
                 <TextView
@@ -176,4 +177,4 @@
                 android:focusable="false"/>
         </LinearLayout>
     </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/tests/GarageModeTestApp/src/com/google/android/car/garagemode/testapp/JobSchedulerWrapper.java b/tests/GarageModeTestApp/src/com/google/android/car/garagemode/testapp/JobSchedulerWrapper.java
index 0a38d7f..5908ed9 100644
--- a/tests/GarageModeTestApp/src/com/google/android/car/garagemode/testapp/JobSchedulerWrapper.java
+++ b/tests/GarageModeTestApp/src/com/google/android/car/garagemode/testapp/JobSchedulerWrapper.java
@@ -48,8 +48,8 @@
     private static final int MAX_SECONDS_PER_JOB = 9 * 60; // 9 minutes
     private static final int JOB_OVERLAP_SECONDS = 10;
 
-    @GuardedBy("sExtendedJobInfoMap")
-    private static final Map<Integer, ExtendedJobInfo> sExtendedJobInfoMap = new HashMap<>();
+    @GuardedBy("mExtendedJobInfoMap")
+    private final Map<Integer, ExtendedJobInfo> mExtendedJobInfoMap = new HashMap<>();
 
     private JobScheduler mJobScheduler;
     private Context mContext;
@@ -57,6 +57,7 @@
     private Handler mHandler;
     private Watchdog mWatchdog;
     private Runnable mRefreshWorker;
+    private boolean mStopWhenFinished = false;
 
     private List<JobInfo> mLastJobsList;
     private List<JobInfo> mNewJobs;
@@ -99,18 +100,36 @@
         LOG.d("Starting JobSchedulerWrapper");
         mHandler = new Handler();
         mRefreshWorker = () -> {
-            refresh();
-            mHandler.postDelayed(mRefreshWorker, 1000);
+            refresh(); // Could nullify mHandler
+            if (mHandler != null) {
+                mHandler.postDelayed(mRefreshWorker, 1000);
+            }
         };
         mHandler.postDelayed(mRefreshWorker, 1000);
     }
 
     public void stop() {
+        boolean canStopNow;
+        synchronized (mExtendedJobInfoMap) {
+            canStopNow = mExtendedJobInfoMap.isEmpty();
+        }
+        if (canStopNow) {
+            stopNow();
+        } else {
+            // There are continuing jobs that we need to schedule
+            // in the future, so don't stop now. We'll stop when
+            // we have scheduled those future jobs.
+            mStopWhenFinished = true;
+        }
+    }
+
+    private void stopNow() {
         LOG.d("Stopping JobSchedulerWrapper");
         mHandler.removeCallbacks(mRefreshWorker);
         mRefreshWorker = null;
         mHandler = null;
         mWatchdog = null;
+        mStopWhenFinished = false;
     }
 
     public void scheduleAJob(
@@ -157,12 +176,10 @@
         editor.putInt(PREFS_NEXT_JOB_ID, jobId + 1);
         editor.commit();
 
-        refresh();
-
         if (extraSeconds > 0) {
             // Remember to schedule another job when this one ends
-            synchronized (sExtendedJobInfoMap) {
-                sExtendedJobInfoMap.put(jobId,
+            synchronized (mExtendedJobInfoMap) {
+                mExtendedJobInfoMap.put(jobId,
                                         new ExtendedJobInfo(extraSeconds,
                                                             networkType,
                                                             isChargingRequired,
@@ -179,6 +196,7 @@
                          isChargingRequired,
                          isIdleRequired);
         }
+        refresh();
     }
 
     private void updateListView() {
@@ -254,8 +272,8 @@
         // extension for any of the newly-completed jobs.
         for (JobInfo completedJobInfo : completedJobsList) {
             ExtendedJobInfo extensionInfo;
-            synchronized (sExtendedJobInfoMap) {
-                extensionInfo = sExtendedJobInfoMap.remove(completedJobInfo.getId());
+            synchronized (mExtendedJobInfoMap) {
+                extensionInfo = mExtendedJobInfoMap.remove(completedJobInfo.getId());
             }
             if (extensionInfo != null) {
                 scheduleAJob(extensionInfo.jobLengthSeconds,
@@ -264,6 +282,17 @@
                              extensionInfo.idleRequired);
             }
         }
+        if (mStopWhenFinished) {
+            boolean canStopNow;
+            synchronized (mExtendedJobInfoMap) {
+                canStopNow = mExtendedJobInfoMap.isEmpty();
+            }
+            if (canStopNow) {
+                // We were asked to stop earlier, but we had more
+                // work to do. That work is now done, so stop now.
+                stopNow();
+            }
+        }
         return completedJobsList;
     }
 
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 c9e85e0..5384005 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
@@ -415,13 +415,17 @@
 
     /**
      * Test that the {@link CarLocationService} deletes location_cache.json when the system resumes
-     * from suspend-to-ram.
+     * from suspend-to-ram if moving.
      */
     @Test
-    public void testDeletesCacheFileUponSuspendExit() throws Exception {
+    public void testDeletesCacheFileUponSuspendExitWhileMoving() throws Exception {
         mCarLocationService.init();
         when(mMockLocationManagerProxy.isLocationEnabled()).thenReturn(false);
+        when(mMockCarDrivingStateService.getCurrentDrivingState()).thenReturn(
+                new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_MOVING, 0));
+        writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
         CompletableFuture<Void> future = new CompletableFuture<>();
+        assertTrue(getLocationCacheFile().exists());
 
         mCarLocationService.onStateChanged(CarPowerStateListener.SUSPEND_EXIT, future);
 
@@ -431,6 +435,26 @@
     }
 
     /**
+     * Test that the {@link CarLocationService} registers for driving state changes when the system
+     * resumes from suspend-to-ram.
+     */
+    @Test
+    public void testRegistersForDrivingStateChangesUponSuspendExit() throws Exception {
+        mCarLocationService.init();
+        when(mMockLocationManagerProxy.isLocationEnabled()).thenReturn(false);
+        when(mMockCarDrivingStateService.getCurrentDrivingState()).thenReturn(
+                new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_PARKED, 0));
+        CompletableFuture<Void> future = new CompletableFuture<>();
+
+        mCarLocationService.onStateChanged(CarPowerStateListener.SUSPEND_EXIT, future);
+
+        assertTrue(future.isDone());
+        verify(mMockLocationManagerProxy, times(0)).isLocationEnabled();
+        // One of the registrations should happen during init and another during onStateChanged.
+        verify(mMockCarDrivingStateService, times(2)).registerDrivingStateChangeListener(any());
+    }
+
+    /**
      * Test that the {@link CarLocationService} deletes location_cache.json when the car enters a
      * moving driving state.
      */
diff --git a/tests/carservice_unit_test/src/com/android/car/trust/BLEMessagePayloadStreamTest.java b/tests/carservice_unit_test/src/com/android/car/trust/BLEMessagePayloadStreamTest.java
index b65bce6..a99b1d7 100644
--- a/tests/carservice_unit_test/src/com/android/car/trust/BLEMessagePayloadStreamTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/trust/BLEMessagePayloadStreamTest.java
@@ -43,7 +43,7 @@
     private static final byte[] TEST_MESSAGE_PAYLOAD = "testMessage".getBytes();
     private static final int TEST_SINGLE_MESSAGE_SIZE =
             TEST_MESSAGE_PAYLOAD.length + BLEMessageV1Factory.getProtoHeaderSize(
-                    OPERATION_TYPE, IS_MESSAGE_ENCRYPTED);
+                    OPERATION_TYPE, TEST_MESSAGE_PAYLOAD.length, IS_MESSAGE_ENCRYPTED);
 
     private BLEMessagePayloadStream mBLEMessagePayloadStream;
     private List<BLEMessage> mBleMessages;
diff --git a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
index f21413e..77d0847 100644
--- a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
@@ -487,26 +487,63 @@
         connection.onServiceConnected(null, new Binder());
         connection.onServiceDisconnected(null);
 
-        verify(mContext).unbindService(connection);
         verify(mConnectionListener).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
 
         Thread.sleep(10);
+        verify(mContext).unbindService(connection);
         verifySystemBind(1);
     }
 
     @Test
-    public void testOnSystemServiceDisconnected_ServiceNotConnected() throws Exception {
+    public void testOnSystemServiceDisconnected_ServiceReboundByAndroid() throws Exception {
         notifySystemUserUnlocked();
         verifySystemBind(1);
         resetContext();
 
         ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
         connection.onServiceDisconnected(null);
 
+        verify(mConnectionListener).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+
+        IBinder binder = new Binder();
+        connection.onServiceConnected(null, binder);
+        verify(mConnectionListener).onClientConnected(eq(SYSTEM_CLIENT_NAME), eq(binder));
+        // No more interactions (verified by tearDown)
+    }
+
+
+    @Test
+    public void testOnSystemServiceBindingDied() throws Exception {
+        notifySystemUserUnlocked();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onServiceDisconnected(null);
+        connection.onBindingDied(null);
+
+        verify(mConnectionListener).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+
+        Thread.sleep(10);
         verify(mContext).unbindService(connection);
+        verifySystemBind(1);
+    }
+
+    @Test
+    public void testOnSystemServiceBindingDied_ServiceNotConnected() throws Exception {
+        notifySystemUserUnlocked();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onBindingDied(null);
+
         verifyZeroInteractions(mConnectionListener);
 
         Thread.sleep(10);
+        verify(mContext).unbindService(connection);
         verifySystemBind(1);
     }
 
@@ -520,26 +557,62 @@
         connection.onServiceConnected(null, new Binder());
         connection.onServiceDisconnected(null);
 
-        verify(mContext).unbindService(connection);
         verify(mConnectionListener).onClientDisconnected(eq(USER_CLIENT_NAME));
 
         Thread.sleep(10);
+        verify(mContext).unbindService(connection);
         verifyUserBind(1);
     }
 
     @Test
-    public void testOnUserServiceDisconnected_ServiceNotConnected() throws Exception {
+    public void testOnUserServiceDisconnected_ServiceReboundByAndroid() throws Exception {
         notifyUserUnlocked();
         verifyUserBind(1);
         resetContext();
 
         ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
         connection.onServiceDisconnected(null);
 
+        verify(mConnectionListener).onClientDisconnected(eq(USER_CLIENT_NAME));
+
+        IBinder binder = new Binder();
+        connection.onServiceConnected(null, binder);
+        verify(mConnectionListener).onClientConnected(eq(USER_CLIENT_NAME), eq(binder));
+        // No more interactions (verified by tearDown)
+    }
+
+    @Test
+    public void testOnUserServiceBindingDied() throws Exception {
+        notifyUserUnlocked();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onServiceDisconnected(null);
+        connection.onBindingDied(null);
+
+        verify(mConnectionListener).onClientDisconnected(eq(USER_CLIENT_NAME));
+
+        Thread.sleep(10);
         verify(mContext).unbindService(connection);
+        verifyUserBind(1);
+    }
+
+    @Test
+    public void testOnUserServiceBindingDied_ServiceNotConnected() throws Exception {
+        notifyUserUnlocked();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onBindingDied(null);
+
         verifyZeroInteractions(mConnectionListener);
 
         Thread.sleep(10);
+        verify(mContext).unbindService(connection);
         verifyUserBind(1);
     }