Merge "Add message time and read status to MAP MCE test page output" into qt-dev
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
index 745d0b3..eed7852 100644
--- a/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
+++ b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
@@ -17,9 +17,10 @@
 package android.car.encryptionrunner;
 
 import android.annotation.NonNull;
-import android.util.Base64;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import com.google.security.cryptauth.lib.securegcm.D2DConnectionContext;
 import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake;
 
@@ -33,8 +34,7 @@
     private static final Ukey2Handshake.HandshakeCipher CIPHER =
             Ukey2Handshake.HandshakeCipher.P256_SHA512;
 
-    // This is from Ukey2HandshakeTest
-    private static final int MAX_AUTH_STRING_LENGTH = 32;
+    private static final int AUTH_STRING_LENGTH = 6;
 
     private Ukey2Handshake mUkey2client;
     private boolean mRunnerIsInvalid;
@@ -104,9 +104,8 @@
 
             String verificationCode = null;
             if (mUkey2client.getHandshakeState() == Ukey2Handshake.State.VERIFICATION_NEEDED) {
-                verificationCode = Base64.encodeToString(
-                        mUkey2client.getVerificationString(MAX_AUTH_STRING_LENGTH),
-                        Base64.DEFAULT);
+                verificationCode = generateReadablePairingCode(
+                        mUkey2client.getVerificationString(AUTH_STRING_LENGTH));
             }
             return HandshakeMessage.newBuilder()
                     .setHandshakeState(getHandshakeState())
@@ -119,6 +118,24 @@
         }
     }
 
+    /**
+     * Returns a human-readable pairing code string generated from the verification bytes. Converts
+     * each byte into a digit with a simple modulo.
+     *
+     * <p>This should match the implementation in the iOS and Android client libraries.
+     */
+    @VisibleForTesting
+    String generateReadablePairingCode(byte[] verificationCode) {
+        StringBuilder outString = new StringBuilder();
+        for (byte b : verificationCode) {
+            int unsignedInt = Byte.toUnsignedInt(b);
+            int digit = unsignedInt % 10;
+            outString.append(digit);
+        }
+
+        return outString.toString();
+    }
+
     private static class UKey2Key implements Key {
 
         private final D2DConnectionContext mConnectionContext;
diff --git a/EncryptionRunner/test/android/car/encryptionrunner/Ukey2EncryptionRunnerTest.java b/EncryptionRunner/test/android/car/encryptionrunner/Ukey2EncryptionRunnerTest.java
new file mode 100644
index 0000000..14ca77d
--- /dev/null
+++ b/EncryptionRunner/test/android/car/encryptionrunner/Ukey2EncryptionRunnerTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.encryptionrunner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class Ukey2EncryptionRunnerTest {
+
+    private Ukey2EncryptionRunner mRunner;
+
+    @Before
+    public void setup() {
+        mRunner = new Ukey2EncryptionRunner();
+    }
+
+    @Test
+    public void generateReadablePairingCode_modsBytesAcrossRange() throws Exception {
+        // 194 is an example of a value that would fail if using signed instead of unsigned ints
+        // 194 -> 11000010
+        // 11000010 -> 194 (unsigned 8-bit int)
+        // 11000010 -> -62 (signed 8-bit int)
+        byte[] bytes = new byte[]{0, 7, (byte) 161, (byte) 194, (byte) 196, (byte) 255};
+        String pairingCode = mRunner.generateReadablePairingCode(bytes);
+
+        assertThat(pairingCode).isEqualTo("071465");
+    }
+}
diff --git a/car-lib/src/android/car/VehiclePropertyIds.java b/car-lib/src/android/car/VehiclePropertyIds.java
index cfc6e34..14642fe 100644
--- a/car-lib/src/android/car/VehiclePropertyIds.java
+++ b/car-lib/src/android/car/VehiclePropertyIds.java
@@ -295,6 +295,11 @@
      */
     public static final int EV_BATTERY_DISPLAY_UNITS = 289408515;
     /**
+     * Speed Units for display
+     * @hide
+     */
+    public static final int VEHICLE_SPEED_DISPLAY_UNITS = 289408516;
+    /**
      * Fuel consumption units for display
      */
     public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364;
@@ -972,6 +977,9 @@
         if (o == READING_LIGHTS_SWITCH) {
             return "READING_LIGHTS_SWITCH";
         }
+        if (o == VEHICLE_SPEED_DISPLAY_UNITS) {
+            return "VEHICLE_SPEED_DISPLAY_UNITS";
+        }
         return "0x" + Integer.toHexString(o);
     }
 }
diff --git a/car-lib/src/android/car/VehicleUnit.java b/car-lib/src/android/car/VehicleUnit.java
index 4fb099a..0c09e87 100644
--- a/car-lib/src/android/car/VehicleUnit.java
+++ b/car-lib/src/android/car/VehicleUnit.java
@@ -72,6 +72,12 @@
     public static final int DEGREES = 0x80;
 
     /** @hide */
+    public static final int MILES_PER_HOUR = 0x90;
+
+    /** @hide */
+    public static final int KILOMETERS_PER_HOUR = 0x91;
+
+    /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
             SHOULD_NOT_USE,
@@ -102,7 +108,9 @@
             KILOWATT_HOUR,
             PSI,
             BAR,
-            DEGREES
+            DEGREES,
+            MILES_PER_HOUR,
+            KILOMETERS_PER_HOUR
     })
     public @interface Enum {}
 
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyEvent.java b/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
index 638fbf2..260d5fa 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
@@ -16,6 +16,7 @@
 
 package android.car.hardware.property;
 
+import android.annotation.NonNull;
 import android.car.hardware.CarPropertyValue;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -68,7 +69,7 @@
     /**
      * Constructor for {@link CarPropertyEvent}.
      */
-    public CarPropertyEvent(int eventType, CarPropertyValue<?> carPropertyValue) {
+    public CarPropertyEvent(@NonNull int eventType, @NonNull CarPropertyValue<?> carPropertyValue) {
         mEventType  = eventType;
         mCarPropertyValue = carPropertyValue;
     }
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyManager.java b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
index 6317c66..85ffd34 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyManager.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
@@ -211,7 +211,7 @@
         return true;
     }
 
-    private class CarPropertyEventListenerToService extends ICarPropertyEventListener.Stub{
+    private static class CarPropertyEventListenerToService extends ICarPropertyEventListener.Stub{
         private final WeakReference<CarPropertyManager> mMgr;
 
         CarPropertyEventListenerToService(CarPropertyManager mgr) {
diff --git a/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java b/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java
index 60b6a62..93519d6 100644
--- a/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java
+++ b/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java
@@ -29,6 +29,7 @@
  * @hide
  */
 public class CarRatedFloatListeners<T> {
+    private static final float NANOSECOND_PER_SECOND = 1000 * 1000 * 1000;
     private final Map<T, Float> mListenersToRate = new HashMap<>(4);
 
     private final Map<T, Long> mListenersUpdateTime = new HashMap<>(4);
@@ -112,7 +113,7 @@
             return true;
         }
         if (nextUpdateTime <= eventTimeStamp) {
-            Float cycle = 1000 / updateRate;
+            Float cycle = NANOSECOND_PER_SECOND / updateRate;
             nextUpdateTime = eventTimeStamp + cycle.longValue();
             mListenersUpdateTime.put(listener, nextUpdateTime);
             return true;
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
index aed2a41..c005792 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
@@ -23,7 +23,11 @@
     <dimen name="car_qs_header_system_icons_area_height">76dp</dimen>
     <dimen name="navigation_bar_height">112dp</dimen>
     <dimen name="navigation_bar_height_landscape">112dp</dimen>
-    <dimen name="status_bar_icon_size">36dp</dimen>
+
+    <!-- Notification icon size in the status bar. -->
+    <dimen name="status_bar_icon_size">32dp</dimen>
+    <!-- System status icon size in the status bar. -->
+    <dimen name="status_bar_system_icon_size">32dp</dimen>
 
     <!-- The size of the right icon. This size is used to down-sample the bitmaps for "large icon". -->
     <dimen name="notification_right_icon_size">@*android:dimen/car_touch_target_size</dimen>
diff --git a/car_product/sepolicy/test/kitchensink_app.te b/car_product/sepolicy/test/kitchensink_app.te
index 8bd5a68..297b3e7 100644
--- a/car_product/sepolicy/test/kitchensink_app.te
+++ b/car_product/sepolicy/test/kitchensink_app.te
@@ -9,6 +9,7 @@
 allow kitchensink_app {
     accessibility_service
     activity_service
+    activity_task_service
     autofill_service
     connectivity_service
     content_service
diff --git a/evs/app/EvsStateControl.cpp b/evs/app/EvsStateControl.cpp
index 50b3aeb..838c8d9 100644
--- a/evs/app/EvsStateControl.cpp
+++ b/evs/app/EvsStateControl.cpp
@@ -58,7 +58,8 @@
     mGearValue.prop       = static_cast<int32_t>(VehicleProperty::GEAR_SELECTION);
     mTurnSignalValue.prop = static_cast<int32_t>(VehicleProperty::TURN_SIGNAL_STATE);
 
-#if 0 // This way we only ever deal with cameras which exist in the system
+#if 1
+    // This way we only ever deal with cameras which exist in the system
     // Build our set of cameras for the states we support
     ALOGD("Requesting camera list");
     mEvs->getCameraList([this, &config](hidl_vec<CameraDesc> cameraList) {
diff --git a/evs/sampleDriver/Android.mk b/evs/sampleDriver/Android.mk
index c4463a9..434f54a 100644
--- a/evs/sampleDriver/Android.mk
+++ b/evs/sampleDriver/Android.mk
@@ -27,6 +27,7 @@
     libhidltransport \
     liblog \
     libutils \
+    libhardware_legacy
 
 LOCAL_INIT_RC := android.hardware.automotive.evs@1.0-sample.rc
 
diff --git a/evs/sampleDriver/EvsEnumerator.cpp b/evs/sampleDriver/EvsEnumerator.cpp
index 96fd067..071aeaa 100644
--- a/evs/sampleDriver/EvsEnumerator.cpp
+++ b/evs/sampleDriver/EvsEnumerator.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2016-2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,8 +19,11 @@
 #include "EvsGlDisplay.h"
 
 #include <dirent.h>
+#include <hardware_legacy/uevent.h>
 
 
+using namespace std::chrono_literals;
+
 namespace android {
 namespace hardware {
 namespace automotive {
@@ -33,10 +36,82 @@
 //        That is to say, this is effectively a singleton despite the fact that HIDL
 //        constructs a new instance for each client.
 std::list<EvsEnumerator::CameraRecord>   EvsEnumerator::sCameraList;
-wp<EvsGlDisplay>                           EvsEnumerator::sActiveDisplay;
+wp<EvsGlDisplay>                         EvsEnumerator::sActiveDisplay;
+std::mutex                               EvsEnumerator::sLock;
+std::condition_variable                  EvsEnumerator::sCameraSignal;
 
-// Number of trials to open the camera.
-static const unsigned int kMaxRetry = 3;
+// Constants
+const auto kEnumerationTimeout = 10s;
+
+void EvsEnumerator::EvsUeventThread(std::atomic<bool>& running) {
+    int status = uevent_init();
+    if (!status) {
+        ALOGE("Failed to initialize uevent handler.");
+        return;
+    }
+
+    char uevent_data[PAGE_SIZE - 2] = {};
+    while (running) {
+        int length = uevent_next_event(uevent_data, static_cast<int32_t>(sizeof(uevent_data)));
+
+        // Ensure double-null termination.
+        uevent_data[length] = uevent_data[length + 1] = '\0';
+
+        const char *action = nullptr;
+        const char *devname = nullptr;
+        const char *subsys = nullptr;
+        char *cp = uevent_data;
+        while (*cp) {
+            // EVS is interested only in ACTION, SUBSYSTEM, and DEVNAME.
+            if (!std::strncmp(cp, "ACTION=", 7)) {
+                action = cp + 7;
+            } else if (!std::strncmp(cp, "SUBSYSTEM=", 10)) {
+                subsys = cp + 10;
+            } else if (!std::strncmp(cp, "DEVNAME=", 8)) {
+                devname = cp + 8;
+            }
+
+            // Advance to after next \0
+            while (*cp++);
+        }
+
+        if (!devname || !subsys || std::strcmp(subsys, "video4linux")) {
+            // EVS expects that the subsystem of enabled video devices is
+            // video4linux.
+            continue;
+        }
+
+        // Update shared list.
+        bool cmd_addition = !std::strcmp(action, "add");
+        bool cmd_removal  = !std::strcmp(action, "remove");
+        {
+            std::string devpath = "/dev/";
+            devpath += devname;
+
+            std::lock_guard<std::mutex> lock(sLock);
+            if (cmd_removal) {
+                for (auto it = sCameraList.begin(); it != sCameraList.end(); ++it) {
+                    if (!devpath.compare(it->desc.cameraId)) {
+                        // There must be no entry with duplicated name.
+                        sCameraList.erase(it);
+                        break;
+                    }
+                }
+            } else if (cmd_addition) {
+                // NOTE: we are here adding new device without a validation
+                // because it always fails to open, b/132164956.
+                sCameraList.emplace_back(devpath.c_str());
+            } else {
+                // Ignore all other actions including "change".
+            }
+
+            // Notify the change.
+            sCameraSignal.notify_all();
+        }
+    }
+
+    return;
+}
 
 EvsEnumerator::EvsEnumerator() {
     ALOGD("EvsEnumerator created");
@@ -61,15 +136,19 @@
         LOG_FATAL("Failed to open /dev folder\n");
     }
     struct dirent* entry;
-    while ((entry = readdir(dir)) != nullptr) {
-        // We're only looking for entries starting with 'video'
-        if (strncmp(entry->d_name, "video", 5) == 0) {
-            std::string deviceName("/dev/");
-            deviceName += entry->d_name;
-            videoCount++;
-            if (qualifyCaptureDevice(deviceName.c_str())) {
-                sCameraList.emplace_back(deviceName.c_str());
-                captureCount++;
+    {
+        std::lock_guard<std::mutex> lock(sLock);
+
+        while ((entry = readdir(dir)) != nullptr) {
+            // We're only looking for entries starting with 'video'
+            if (strncmp(entry->d_name, "video", 5) == 0) {
+                std::string deviceName("/dev/");
+                deviceName += entry->d_name;
+                videoCount++;
+                if (qualifyCaptureDevice(deviceName.c_str())) {
+                    sCameraList.emplace_back(deviceName.c_str());
+                    captureCount++;
+                }
             }
         }
     }
@@ -80,19 +159,16 @@
 // Methods from ::android::hardware::automotive::evs::V1_0::IEvsEnumerator follow.
 Return<void> EvsEnumerator::getCameraList(getCameraList_cb _hidl_cb)  {
     ALOGD("getCameraList");
-
-    if (sCameraList.size() < 1) {
-        // WAR: this assumes that the device has at least one compatible camera and
-        // therefore keeps trying until it succeeds to open.
-        // TODO: this is required for external USB camera so would be better to
-        // subscribe hot-plug event.
-        unsigned tries = 0;
-        ALOGI("No camera is available; enumerate devices again.");
-        while (sCameraList.size() < 1 && tries++ < kMaxRetry) {
-            enumerateDevices();
-
-            // TODO: remove this.
-            usleep(5000);
+    {
+        std::unique_lock<std::mutex> lock(sLock);
+        if (sCameraList.size() < 1) {
+            // No qualified device has been found.  Wait until new device is ready,
+            // for 10 seconds.
+            if (!sCameraSignal.wait_for(lock,
+                                        kEnumerationTimeout,
+                                        []{ return sCameraList.size() > 0; })) {
+                ALOGD("Timer expired.  No new device has been added.");
+            }
         }
     }
 
diff --git a/evs/sampleDriver/EvsEnumerator.h b/evs/sampleDriver/EvsEnumerator.h
index e7e94d4..01a57bd 100644
--- a/evs/sampleDriver/EvsEnumerator.h
+++ b/evs/sampleDriver/EvsEnumerator.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2016-2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@
 #include <android/hardware/automotive/evs/1.0/IEvsCamera.h>
 
 #include <list>
+#include <thread>
+#include <atomic>
 
 
 namespace android {
@@ -48,6 +50,9 @@
     // Implementation details
     EvsEnumerator();
 
+    // Listen to video device uevents
+    static void EvsUeventThread(std::atomic<bool>& running);
+
 private:
     struct CameraRecord {
         CameraDesc          desc;
@@ -70,6 +75,9 @@
     static std::list<CameraRecord> sCameraList;
 
     static wp<EvsGlDisplay>        sActiveDisplay; // Weak pointer. Object destructs if client dies.
+
+    static std::mutex              sLock;          // Mutex on shared camera device list.
+    static std::condition_variable sCameraSignal;  // Signal on camera device addition.
 };
 
 } // namespace implementation
diff --git a/evs/sampleDriver/service.cpp b/evs/sampleDriver/service.cpp
index 41be742..a444686 100644
--- a/evs/sampleDriver/service.cpp
+++ b/evs/sampleDriver/service.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2016-2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
  */
 
 #include <unistd.h>
+#include <atomic>
 
 #include <hidl/HidlTransportSupport.h>
 #include <utils/Errors.h>
@@ -45,6 +46,9 @@
 
     configureRpcThreadpool(1, true /* callerWillJoin */);
 
+    std::atomic<bool> running { true };
+    std::thread ueventHandler(EvsEnumerator::EvsUeventThread, std::ref(running));
+
     // Register our service -- if somebody is already registered by our name,
     // they will be killed (their thread pool will throw an exception).
     status_t status = service->registerAsService(kEnumeratorServiceName);
@@ -55,6 +59,12 @@
         ALOGE("Could not register service %s (%d).", kEnumeratorServiceName, status);
     }
 
+    // Exit a uevent handler thread.
+    running = false;
+    if (ueventHandler.joinable()) {
+        ueventHandler.join();
+    }
+
     // In normal operation, we don't expect the thread pool to exit
     ALOGE("EVS Hardware Enumerator is shutting down");
     return 1;
diff --git a/evs/sepolicy/evs_app.te b/evs/sepolicy/evs_app.te
index 098499a..b5e3c95 100644
--- a/evs/sepolicy/evs_app.te
+++ b/evs/sepolicy/evs_app.te
@@ -18,3 +18,7 @@
 allow evs_app gpu_device:chr_file rw_file_perms;
 allow evs_app ion_device:chr_file r_file_perms;
 allow evs_app system_file:dir r_dir_perms;
+
+# Allow use of binder and find surfaceflinger
+binder_use(evs_app);
+allow evs_app surfaceflinger_service:service_manager find;
diff --git a/evs/sepolicy/evs_driver.te b/evs/sepolicy/evs_driver.te
index dcf6700..3d5263e 100644
--- a/evs/sepolicy/evs_driver.te
+++ b/evs/sepolicy/evs_driver.te
@@ -18,3 +18,6 @@
 binder_call(hal_evs_driver, surfaceflinger);
 allow hal_evs_driver surfaceflinger_service:service_manager find;
 allow hal_evs_driver ion_device:chr_file r_file_perms;
+
+# Allow the driver to access kobject uevents
+allow hal_evs_driver self:netlink_kobject_uevent_socket create_socket_perms_no_ioctl;
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index 309f393..a4e0d6e 100644
--- a/service/src/com/android/car/CarMediaService.java
+++ b/service/src/com/android/car/CarMediaService.java
@@ -15,12 +15,12 @@
  */
 package com.android.car;
 
+import android.app.ActivityManager;
 import android.car.media.CarMediaManager;
 import android.car.media.CarMediaManager.MediaSourceChangedListener;
 import android.car.media.ICarMedia;
 import android.car.media.ICarMediaSourceListener;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -32,9 +32,8 @@
 import android.media.session.MediaSession;
 import android.media.session.MediaSession.Token;
 import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
 import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.service.media.MediaBrowserService;
@@ -44,7 +43,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.car.media.MediaBrowserConnector;
 import com.android.car.user.CarUserService;
 
 import java.io.PrintWriter;
@@ -70,25 +68,23 @@
 public class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
 
     private static final String SOURCE_KEY = "media_source";
+    private static final String PLAYBACK_STATE_KEY = "playback_state";
     private static final String SHARED_PREF = "com.android.car.media.car_media_service";
     private static final String PACKAGE_NAME_SEPARATOR = ",";
 
     private Context mContext;
     private final MediaSessionManager mMediaSessionManager;
-    private final MediaSessionUpdater mMediaSessionUpdater;
+    private MediaSessionUpdater mMediaSessionUpdater;
     private String mPrimaryMediaPackage;
     private SharedPreferences mSharedPrefs;
-    // MediaController for the primary media source. Can be null if the primary media source has not
-    // played any media yet.
-    private MediaController mPrimaryMediaController;
-    private final MediaBrowserConnector mMediaBrowserConnector;
+    // MediaController for the current active user's active media session. This controller can be
+    // null if playback has not been started yet.
+    private MediaController mActiveUserMediaController;
+    private SessionChangedListener mSessionsListener;
 
     private RemoteCallbackList<ICarMediaSourceListener> mMediaSourceListeners =
             new RemoteCallbackList();
 
-    private HandlerThread mHandlerThread;
-    private Handler mHandler;
-
     /** The package name of the last media source that was removed while being primary. */
     private String mRemovedMediaSourcePackage;
 
@@ -120,16 +116,10 @@
         }
     };
 
-    private final MediaBrowserConnector.Callback mConnectedBrowserCallback = browser -> {
-        if (browser != null && browser.isConnected()) {
-            mPrimaryMediaController =
-                    new MediaController(mContext, browser.getSessionToken());
-            TransportControls controls = mPrimaryMediaController.getTransportControls();
-            if (controls != null) {
-                controls.prepare();
-            }
-        } else {
-            mPrimaryMediaController = null;
+    private BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            updateMediaSessionCallbackForCurrentUser();
         }
     };
 
@@ -145,14 +135,11 @@
         filter.addDataScheme("package");
         mContext.registerReceiver(mPackageRemovedReceiver, filter);
 
-        mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessions(null));
-        mMediaSessionManager.addOnActiveSessionsChangedListener(
-                controllers -> mMediaSessionUpdater.registerCallbacks(controllers), null);
+        IntentFilter userSwitchFilter = new IntentFilter();
+        userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
+        mContext.registerReceiver(mUserSwitchReceiver, userSwitchFilter);
 
-        mMediaBrowserConnector = new MediaBrowserConnector(mContext, mConnectedBrowserCallback);
-        mHandlerThread = new HandlerThread(CarLog.TAG_MEDIA);
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
+        updateMediaSessionCallbackForCurrentUser();
     }
 
     @Override
@@ -173,12 +160,13 @@
     public void dump(PrintWriter writer) {
         writer.println("*CarMediaService*");
         writer.println("\tCurrent media package: " + mPrimaryMediaPackage);
-        if (mPrimaryMediaController != null) {
-            writer
-                .println("\tCurrent media controller: " + mPrimaryMediaController.getPackageName());
+        if (mActiveUserMediaController != null) {
+            writer.println(
+                    "\tCurrent media controller: " + mActiveUserMediaController.getPackageName());
         }
         writer.println("\tNumber of active media sessions: "
-                + mMediaSessionManager.getActiveSessions(null).size());
+                + mMediaSessionManager.getActiveSessionsForUser(null,
+                        ActivityManager.getCurrentUser()).size());
     }
 
     /**
@@ -217,18 +205,46 @@
         mMediaSourceListeners.unregister(callback);
     }
 
+    private void updateMediaSessionCallbackForCurrentUser() {
+        if (mSessionsListener != null) {
+            mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);
+        }
+        mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());
+        mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsListener, null,
+                ActivityManager.getCurrentUser(), null);
+        mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser(
+                null, ActivityManager.getCurrentUser()));
+    }
+
     /**
      * Attempts to stop the current source using MediaController.TransportControls.stop()
      */
     private void stop() {
-        if (mPrimaryMediaController != null) {
-            TransportControls controls = mPrimaryMediaController.getTransportControls();
+        if (mActiveUserMediaController != null) {
+            TransportControls controls = mActiveUserMediaController.getTransportControls();
             if (controls != null) {
                 controls.stop();
             }
         }
     }
 
+    private class SessionChangedListener implements OnActiveSessionsChangedListener {
+        private final int mCurrentUser;
+
+        SessionChangedListener(int currentUser) {
+            mCurrentUser = currentUser;
+        }
+
+        @Override
+        public void onActiveSessionsChanged(List<MediaController> controllers) {
+            if (ActivityManager.getCurrentUser() != mCurrentUser) {
+                Log.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser);
+                return;
+            }
+            mMediaSessionUpdater.registerCallbacks(controllers);
+        }
+    }
+
     private class MediaControllerCallback extends MediaController.Callback {
 
         private final MediaController mMediaController;
@@ -288,6 +304,11 @@
 
             mCallbacks = updatedCallbacks;
             updatePrimaryMediaSourceWithCurrentlyPlaying(additions);
+            // If there are no playing media sources, and we don't currently have the controller
+            // for the active source, check the new active sessions for a matching controller.
+            if (mActiveUserMediaController == null) {
+                updateActiveMediaController(additions);
+            }
         }
 
         /**
@@ -311,17 +332,8 @@
         stop();
 
         mPrimaryMediaPackage = packageName;
-        mPrimaryMediaController = null;
-        if (packageName != null) {
-            String browseServiceName = getBrowseServiceClassName(packageName);
-            if (browseServiceName != null) {
-                mHandler.post(() -> mMediaBrowserConnector.connectTo(
-                        new ComponentName(packageName, browseServiceName)));
-            } else {
-                Log.e(CarLog.TAG_MEDIA, "Can't connect to BrowseService for media source: "
-                        + packageName);
-            }
-        }
+        updateActiveMediaController(mMediaSessionManager
+                .getActiveSessionsForUser(null, ActivityManager.getCurrentUser()));
 
         if (mSharedPrefs != null) {
             if (!TextUtils.isEmpty(mPrimaryMediaPackage)) {
@@ -348,6 +360,13 @@
         mMediaSourceListeners.finishBroadcast();
     }
 
+    private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            savePlaybackState(state);
+        }
+    };
+
     /**
      * Finds the currently playing media source, then updates the active source if different
      */
@@ -422,4 +441,31 @@
         String[] packageNames = serialized.split(PACKAGE_NAME_SEPARATOR);
         return new ArrayDeque(Arrays.asList(packageNames));
     }
+
+    private void savePlaybackState(PlaybackState state) {
+        mSharedPrefs.edit().putInt(PLAYBACK_STATE_KEY, state != null ? state.getState()
+                : PlaybackState.STATE_NONE).apply();
+    }
+
+    /**
+     * Updates active media controller from the list that has the same package name as the primary
+     * media package. Clears callback and resets media controller to null if not found.
+     */
+    private void updateActiveMediaController(List<MediaController> mediaControllers) {
+        if (mPrimaryMediaPackage == null) {
+            return;
+        }
+        if (mActiveUserMediaController != null) {
+            mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
+            mActiveUserMediaController = null;
+        }
+        for (MediaController controller : mediaControllers) {
+            if (mPrimaryMediaPackage.equals(controller.getPackageName())) {
+                mActiveUserMediaController = controller;
+                savePlaybackState(mActiveUserMediaController.getPlaybackState());
+                mActiveUserMediaController.registerCallback(mMediaControllerCallback);
+                return;
+            }
+        }
+    }
 }
diff --git a/service/src/com/android/car/CarPropertyService.java b/service/src/com/android/car/CarPropertyService.java
index 961ba5e..ed24251 100644
--- a/service/src/com/android/car/CarPropertyService.java
+++ b/service/src/com/android/car/CarPropertyService.java
@@ -206,15 +206,20 @@
         List<CarPropertyEvent> events = new LinkedList<CarPropertyEvent>();
         if (mConfigs.get(propId).isGlobalProperty()) {
             CarPropertyValue value = mHal.getProperty(propId, 0);
-            CarPropertyEvent event = new CarPropertyEvent(
+            // CarPropertyEvent without a CarPropertyValue can not be used by any listeners.
+            if (value != null) {
+                CarPropertyEvent event = new CarPropertyEvent(
                     CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
-            events.add(event);
+                events.add(event);
+            }
         } else {
             for (int areaId : mConfigs.get(propId).getAreaIds()) {
                 CarPropertyValue value = mHal.getProperty(propId, areaId);
-                CarPropertyEvent event = new CarPropertyEvent(
+                if (value != null) {
+                    CarPropertyEvent event = new CarPropertyEvent(
                         CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
-                events.add(event);
+                    events.add(event);
+                }
             }
         }
         try {
diff --git a/service/src/com/android/car/hal/PropertyHalService.java b/service/src/com/android/car/hal/PropertyHalService.java
index ad81507..545fc2b 100644
--- a/service/src/com/android/car/hal/PropertyHalService.java
+++ b/service/src/com/android/car/hal/PropertyHalService.java
@@ -303,6 +303,9 @@
         }
         if (listener != null) {
             for (VehiclePropValue v : values) {
+                if (v == null) {
+                    continue;
+                }
                 int mgrPropId = halToManagerPropId(v.prop);
                 if (mgrPropId == NOT_SUPPORTED_PROPERTY) {
                     Log.e(TAG, "Property is not supported: 0x" + toHexString(v.prop));
diff --git a/service/src/com/android/car/hal/PropertyHalServiceIds.java b/service/src/com/android/car/hal/PropertyHalServiceIds.java
index ca90f78..5409b4d 100644
--- a/service/src/com/android/car/hal/PropertyHalServiceIds.java
+++ b/service/src/com/android/car/hal/PropertyHalServiceIds.java
@@ -400,6 +400,9 @@
         mProps.put(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME, new Pair<>(
                 Car.PERMISSION_READ_DISPLAY_UNITS,
                 Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+        mProps.put(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, new Pair<>(
+                Car.PERMISSION_READ_DISPLAY_UNITS,
+                Car.PERMISSION_CONTROL_DISPLAY_UNITS));
     }
 
     /**
diff --git a/service/src/com/android/car/media/MediaBrowserConnector.java b/service/src/com/android/car/media/MediaBrowserConnector.java
deleted file mode 100644
index 0cc03f6..0000000
--- a/service/src/com/android/car/media/MediaBrowserConnector.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.util.Log;
-
-/**
- * A helper class to connect to a single {@link MediaBrowser}. Connecting to a new one
- * automatically disconnects the previous browser. Changes of the currently connected browser are
- * sent via {@link MediaBrowserConnector.Callback}.
- */
-public class MediaBrowserConnector {
-
-    private static final String TAG = "MediaBrowserConnector";
-
-    /** The callback to receive the currently connected {@link MediaBrowser}. */
-    public interface Callback {
-        /** When disconnecting, the given browser will be null. */
-        void onConnectedBrowserChanged(@Nullable MediaBrowser browser);
-    }
-
-    private final Context mContext;
-    private final Callback mCallback;
-
-    @Nullable private ComponentName mBrowseService;
-    @Nullable private MediaBrowser mBrowser;
-
-    /**
-     * Create a new MediaBrowserConnector.
-     *
-     * @param context The Context with which to build MediaBrowsers.
-     */
-    public MediaBrowserConnector(@NonNull Context context, @NonNull Callback callback) {
-        mContext = context;
-        mCallback = callback;
-    }
-
-    /** Counter so callbacks from obsolete connections can be ignored. */
-    private int mBrowserConnectionCallbackCounter = 0;
-
-    private class BrowserConnectionCallback extends MediaBrowser.ConnectionCallback {
-
-        private final int mSequenceNumber = ++mBrowserConnectionCallbackCounter;
-        private final String mCallbackPackage = mBrowseService.getPackageName();
-
-        private boolean isValidCall(String method) {
-            if (mSequenceNumber != mBrowserConnectionCallbackCounter) {
-                Log.e(TAG, "Ignoring callback " + method + " for " + mCallbackPackage + " seq: "
-                        + mSequenceNumber + " current: " + mBrowserConnectionCallbackCounter
-                        + " current: " + mBrowseService.getPackageName());
-                return false;
-            } else if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, method + " " + mBrowseService.getPackageName());
-            }
-            return true;
-        }
-
-        @Override
-        public void onConnected() {
-            if (isValidCall("onConnected")) {
-                mCallback.onConnectedBrowserChanged(mBrowser);
-            }
-        }
-
-        @Override
-        public void onConnectionFailed() {
-            if (isValidCall("onConnectionFailed")) {
-                mCallback.onConnectedBrowserChanged(null);
-            }
-        }
-
-        @Override
-        public void onConnectionSuspended() {
-            if (isValidCall("onConnectionSuspended")) {
-                mCallback.onConnectedBrowserChanged(null);
-            }
-        }
-    }
-
-    /**
-     * Creates and connects a new {@link MediaBrowser} if the given {@link ComponentName}
-     * isn't null. If needed, the previous browser is disconnected.
-     * @param browseService the ComponentName of the media browser service.
-     * @see MediaBrowser#MediaBrowser(Context, ComponentName,
-     * MediaBrowser.ConnectionCallback, android.os.Bundle)
-     */
-    public void connectTo(@Nullable ComponentName browseService) {
-        if (mBrowser != null && mBrowser.isConnected()) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Disconnecting: " + mBrowseService.getPackageName());
-            }
-            mCallback.onConnectedBrowserChanged(null);
-            mBrowser.disconnect();
-        }
-
-        mBrowseService = browseService;
-        if (mBrowseService != null) {
-            mBrowser = createMediaBrowser(mBrowseService, new BrowserConnectionCallback());
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Connecting to: " + mBrowseService.getPackageName());
-            }
-            try {
-                mBrowser.connect();
-            } catch (IllegalStateException ex) {
-                // Is this comment still valid ?
-                // Ignore: MediaBrowse could be in an intermediate state (not connected, but not
-                // disconnected either.). In this situation, trying to connect again can throw
-                // this exception, but there is no way to know without trying.
-                Log.e(TAG, "Connection exception: " + ex);
-            }
-        } else {
-            mBrowser = null;
-        }
-    }
-
-    // Override for testing.
-    @NonNull
-    protected MediaBrowser createMediaBrowser(@NonNull ComponentName browseService,
-            @NonNull MediaBrowser.ConnectionCallback callback) {
-        return new MediaBrowser(mContext, browseService, callback, null);
-    }
-}
diff --git a/service/src/com/android/car/trust/BLEMessagePayloadStream.java b/service/src/com/android/car/trust/BLEMessagePayloadStream.java
new file mode 100644
index 0000000..1f1fdb1
--- /dev/null
+++ b/service/src/com/android/car/trust/BLEMessagePayloadStream.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.trust;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import com.android.car.trust.BLEStream.BLEMessage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Manage a stream in which the {@code payload} field of
+ * {@link com.android.car.trust.BLEStream.BLEMessage}
+ * is written to.
+ */
+class BLEMessagePayloadStream {
+    private static final String TAG = "BLEMessagePayloadStream";
+    private ByteArrayOutputStream mPendingData = new ByteArrayOutputStream();
+    private boolean mIsComplete;
+
+    /**
+     * Clears this data stream.
+     */
+    public void reset() {
+        mPendingData.reset();
+    }
+
+    /**
+     * Extracts the payload from the given bytes and writes it to the stream.
+     *
+     * @param value The bytes that represent a {@link com.android.car.trust.BLEStream.BLEMessage}
+     */
+    public void write(byte[] value) {
+        try {
+            BLEMessage message = BLEMessage.parseFrom(value);
+            mPendingData.write(message.getPayload().toByteArray());
+            if (message.getPacketNumber() == message.getTotalPackets()) {
+                mIsComplete = true;
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Can not parse BLE message", e);
+        }
+    }
+
+    /**
+     * Returns {@code true} if a complete payload has been formed.
+     */
+    public boolean isComplete() {
+        return mIsComplete;
+    }
+
+    /**
+     * Returns the current contents of a stream as a byte array.
+     */
+    @NonNull
+    public byte[] toByteArray() {
+        return mPendingData.toByteArray();
+    }
+}
diff --git a/service/src/com/android/car/trust/BLEMessageV1Factory.java b/service/src/com/android/car/trust/BLEMessageV1Factory.java
new file mode 100644
index 0000000..c0b11d4
--- /dev/null
+++ b/service/src/com/android/car/trust/BLEMessageV1Factory.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.trust;
+
+import android.util.Log;
+
+import com.android.car.trust.BLEStream.BLEMessage;
+import com.android.car.trust.BLEStream.BLEMessage.OperationType;
+import com.android.car.trust.protobuf.ByteString;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Methods for creating {@link BLEStream} protos
+ */
+class BLEMessageV1Factory {
+
+    private static final String TAG = "BLEMessageFactory";
+
+    /**
+     * The size in bytes of a {@code fixed32} field in the proto.
+     * See this <a href="https://developers.google.com/protocol-buffers/docs/encoding">site</a> for
+     * more details.
+     */
+    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;
+
+    /**
+     * The bytes needed to encode the field number in the proto.
+     *
+     * <p>Since the v1 proto only has 6 fields, it will only take 1 additional byte to encode.
+     */
+    private static final int FIELD_NUMBER_ENCODING_SIZE = 1;
+    /**
+     * Current version of the proto.
+     */
+    private static final int PROTOCOL_VERSION = 1;
+    /**
+     * The size of the version field in the proto.
+     *
+     * <p>The version field is a {@code variant} and thus, its size can vary between 1-5 bytes.
+     * Since the version is {@code 1}, it will only take 1 byte to encode + 1 byte for the field
+     * number.
+     */
+    private static final int VERSION_SIZE = getEncodedSize(PROTOCOL_VERSION)
+            + FIELD_NUMBER_ENCODING_SIZE;
+    /**
+     * The size of fields in the header that do not change depending on their value.
+     *
+     * <p>The fixed fields are:
+     *
+     * <ol>
+     * <li>Version (1 byte + 1 byte for field)
+     * <li>Packet number (4 bytes + 1 byte for field)
+     * <li>Total packets (4 bytes + 1 byte for field)
+     * </ol>
+     *
+     * <p>Note, the version code is an {@code Int32Value} and thus, can vary, but it is always a set
+     * value at compile time. Thus, it can be calculated statically.
+     */
+    private static final int CONSTANT_HEADER_FIELD_SIZE = VERSION_SIZE
+            + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE)
+            + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE);
+
+    private BLEMessageV1Factory() {
+    }
+
+    /**
+     * Method used to generate a single message, the packet number and total packets will set to 1
+     * by default
+     *
+     * @param payload   The data object to use as the
+     *                  {@link com.android.car.trust.BLEStream.BLEMessage}
+     *                  payload
+     * @param operation The operation this message represents
+     * @return The generated {@link com.android.car.trust.BLEStream.BLEMessage}
+     */
+    private static BLEMessage makeBLEMessage(byte[] payload, OperationType operation,
+            boolean isPayloadEncrypted) {
+        return BLEMessage.newBuilder()
+                .setVersion(PROTOCOL_VERSION)
+                .setOperation(operation)
+                .setPacketNumber(1)
+                .setTotalPackets(1)
+                .setIsPayloadEncrypted(isPayloadEncrypted)
+                .setPayload(ByteString.copyFrom(payload))
+                .build();
+    }
+
+    /**
+     * Split given data if necessary to fit within the given {@code maxSize}
+     *
+     * @param payload   The payload to potentially split across multiple {@link
+     *                  com.android.car.trust.BLEStream.BLEMessage}s
+     * @param operation The operation this message represents
+     * @param maxSize   The maximum size of each chunk
+     * @return An array of {@link com.android.car.trust.BLEStream.BLEMessage}s
+     */
+    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) {
+            bleMessages.add(makeBLEMessage(payload, operation, isPayloadEncrypted));
+            return bleMessages;
+        }
+        int totalPackets = (int) Math.ceil((double) payloadLength / maxPayloadSize);
+        int start = 0;
+        int end = maxPayloadSize;
+        for (int i = 0; i < totalPackets; i++) {
+            bleMessages.add(BLEMessage.newBuilder()
+                    .setVersion(PROTOCOL_VERSION)
+                    .setOperation(operation)
+                    .setPacketNumber(i + 1)
+                    .setTotalPackets(totalPackets)
+                    .setIsPayloadEncrypted(isPayloadEncrypted)
+                    .setPayload(ByteString.copyFrom(Arrays.copyOfRange(payload, start, end)))
+                    .build());
+            start = end;
+            end = Math.min(start + maxPayloadSize, payloadLength);
+        }
+        return bleMessages;
+    }
+
+    /**
+     * Returns the header size for the proto in bytes. This method assumes that the proto
+     * contain a payload.
+     */
+    public static int getProtoHeaderSize(OperationType operation, boolean isPayloadEncrypted) {
+        int isPayloadEncryptedFieldSize =
+                isPayloadEncrypted ? (BOOLEAN_FIELD_ENCODING_SIZE + FIELD_NUMBER_ENCODING_SIZE) : 0;
+        int operationSize = getEncodedSize(operation.getNumber()) + FIELD_NUMBER_ENCODING_SIZE;
+        return CONSTANT_HEADER_FIELD_SIZE + operationSize + isPayloadEncryptedFieldSize
+                + ADDITIONAL_PAYLOAD_ENCODING_SIZE;
+    }
+
+    /**
+     * The methods in this section are taken from
+     * google3/third_party/swift/swift_protobuf/Sources/SwiftProtobuf/Variant.swift.
+     * It should be kept in sync as long as the proto version remains the same.
+     *
+     * <p>Computes the number of bytes that would be needed to store a 32-bit variant. Negative
+     * value is not considered because all proto values should be positive.
+     *
+     * @param value the data that need to be encoded
+     * @return the size of the encoded data
+     */
+    private static int getEncodedSize(int value) {
+        if (value < 0) {
+            Log.e(TAG, "Get a negative value from proto");
+            return 10;
+        }
+        if ((value & (~0 << 7)) == 0) {
+            return 1;
+        }
+        if ((value & (~0 << 14)) == 0) {
+            return 2;
+        }
+        if ((value & (~0 << 21)) == 0) {
+            return 3;
+        }
+        if ((value & (~0 << 28)) == 0) {
+            return 4;
+        }
+        return 5;
+    }
+}
diff --git a/service/src/com/android/car/trust/BleManager.java b/service/src/com/android/car/trust/BleManager.java
index 4a8bcbc..61e1be6 100644
--- a/service/src/com/android/car/trust/BleManager.java
+++ b/service/src/com/android/car/trust/BleManager.java
@@ -54,10 +54,12 @@
     private static final int BLE_RETRY_LIMIT = 5;
     private static final int BLE_RETRY_INTERVAL_MS = 1000;
 
-    // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.generic_access.xml
+    // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
+    // .service.generic_access.xml
     private static final UUID GENERIC_ACCESS_PROFILE_UUID =
             UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");
-    //https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.gap.device_name.xml
+    //https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
+    // .characteristic.gap.device_name.xml
     private static final UUID DEVICE_NAME_UUID =
             UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb");
 
@@ -81,8 +83,8 @@
      * <p>It is possible that BLE service is still in TURNING_ON state when this method is invoked.
      * Therefore, several retries will be made to ensure advertising is started.
      *
-     * @param service {@link BluetoothGattService} that will be discovered by clients
-     * @param data {@link AdvertiseData} data to advertise
+     * @param service           {@link BluetoothGattService} that will be discovered by clients
+     * @param data              {@link AdvertiseData} data to advertise
      * @param advertiseCallback {@link AdvertiseCallback} callback for advertiser
      */
     protected void startAdvertising(BluetoothGattService service, AdvertiseData data,
@@ -220,6 +222,14 @@
     }
 
     /**
+     * Triggered if a remote client has requested to change the MTU for a given connection.
+     *
+     * @param size The new MTU size.
+     */
+    protected void onMtuSizeChanged(int size) {
+    }
+
+    /**
      * Triggered when a device (GATT client) connected.
      *
      * @param device Remote device that connected on BLE.
@@ -313,6 +323,15 @@
                     onCharacteristicWrite(device, requestId, characteristic,
                             preparedWrite, responseNeeded, offset, value);
                 }
+
+                @Override
+                public void onMtuChanged(BluetoothDevice device, int mtu) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
+                    }
+                    onMtuSizeChanged(mtu);
+                }
+
             };
 
     private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@@ -337,7 +356,7 @@
                     if (Log.isLoggable(TAG, Log.DEBUG)) {
                         Log.d(TAG,
                                 "Connection state not connecting or disconnecting; ignoring: "
-                                + newState);
+                                        + newState);
                     }
             }
         }
diff --git a/service/src/com/android/car/trust/CarTrustAgentBleManager.java b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
index 9e0785b..c47f527 100644
--- a/service/src/com/android/car/trust/CarTrustAgentBleManager.java
+++ b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
@@ -33,9 +33,12 @@
 import com.android.car.CarLocalServices;
 import com.android.car.R;
 import com.android.car.Utils;
+import com.android.car.trust.BLEStream.BLEMessage;
+import com.android.car.trust.BLEStream.BLEMessage.OperationType;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.List;
 import java.util.UUID;
 
 /**
@@ -70,6 +73,7 @@
     private String mOriginalBluetoothName;
     private byte[] mUniqueId;
     private String mRandomName;
+    private int mMtuSize = 20;
 
     // Enrollment Service and Characteristic UUIDs
     private UUID mEnrollmentServiceUuid;
@@ -83,6 +87,8 @@
     private UUID mUnlockTokenHandleUuid;
     private BluetoothGattService mUnlockGattService;
 
+    private BLEMessagePayloadStream mBleMessagePayloadStream = new BLEMessagePayloadStream();
+
     CarTrustAgentBleManager(Context context) {
         super(context);
     }
@@ -117,6 +123,11 @@
     }
 
     @Override
+    protected void onMtuSizeChanged(int size) {
+        mMtuSize = size;
+    }
+
+    @Override
     public void onCharacteristicWrite(BluetoothDevice device, int requestId,
             BluetoothGattCharacteristic characteristic, boolean preparedWrite,
             boolean responseNeeded, int offset, byte[] value) {
@@ -124,19 +135,28 @@
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
         }
+        // This write operation is not thread safe individually, but is guarded by the callback
+        // here.
+        mBleMessagePayloadStream.write(value);
+        if (!mBleMessagePayloadStream.isComplete()) {
+            return;
+        }
         if (uuid.equals(mEnrollmentClientWriteUuid)) {
             if (getEnrollmentService() != null) {
-                getEnrollmentService().onEnrollmentDataReceived(value);
+                getEnrollmentService().onEnrollmentDataReceived(
+                        mBleMessagePayloadStream.toByteArray());
             }
         } else if (uuid.equals(mUnlockEscrowTokenUuid)) {
             if (getUnlockService() != null) {
-                getUnlockService().onUnlockTokenReceived(value);
+                getUnlockService().onUnlockTokenReceived(mBleMessagePayloadStream.toByteArray());
+
             }
         } else if (uuid.equals(mUnlockTokenHandleUuid)) {
             if (getUnlockService() != null) {
-                getUnlockService().onUnlockHandleReceived(value);
+                getUnlockService().onUnlockHandleReceived(mBleMessagePayloadStream.toByteArray());
             }
         }
+        mBleMessagePayloadStream.reset();
     }
 
     @Override
@@ -316,24 +336,33 @@
     }
 
     void disconnectRemoteDevice() {
+        mBleMessagePayloadStream.reset();
         stopGattServer();
     }
 
     /**
      * Sends the given message to the specified device.
      *
-     * @param device The device to send the message to.
+     * @param device  The device to send the message to.
      * @param message A message to send.
      */
-    void sendMessage(BluetoothDevice device, byte[] message) {
+    void sendMessage(BluetoothDevice device, byte[] message, OperationType operation,
+            boolean isPayloadEncrypted) {
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "sendMessage to: " + device.getAddress());
         }
-
         BluetoothGattCharacteristic serverCharacteristic = mEnrollmentGattService
                 .getCharacteristic(mEnrollmentServerWriteUuid);
-        serverCharacteristic.setValue(message);
-        notifyCharacteristicChanged(device, serverCharacteristic, false);
+        List<BLEMessage> bleMessages = BLEMessageV1Factory.makeBLEMessages(message, operation,
+                mMtuSize, isPayloadEncrypted);
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "sending " + bleMessages.size() + " messages to device");
+        }
+        for (BLEMessage bleMessage : bleMessages) {
+            // TODO(b/131719066) get acknowledgement from the phone then continue to send packets
+            serverCharacteristic.setValue(bleMessage.toByteArray());
+            notifyCharacteristicChanged(device, serverCharacteristic, false);
+        }
     }
 
     private final AdvertiseCallback mEnrollmentAdvertisingCallback = new AdvertiseCallback() {
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index 635d38f..39ec796 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -42,6 +42,7 @@
 
 import com.android.car.R;
 import com.android.car.Utils;
+import com.android.car.trust.BLEStream.BLEMessage.OperationType;
 import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
@@ -67,7 +68,6 @@
  */
 public class CarTrustAgentEnrollmentService extends ICarTrustAgentEnrollment.Stub {
     private static final String TAG = "CarTrustAgentEnroll";
-    private static final String FAKE_AUTH_STRING = "000000";
     private static final String TRUSTED_DEVICE_ENROLLMENT_ENABLED_KEY =
             "trusted_device_enrollment_enabled";
     private static final byte[] CONFIRMATION_SIGNAL = "True".getBytes();
@@ -87,8 +87,6 @@
     private Object mRemoteDeviceLock = new Object();
     @GuardedBy("mRemoteDeviceLock")
     private BluetoothDevice mRemoteEnrollmentDevice;
-    @GuardedBy("this")
-    private boolean mEnrollmentHandshakeAccepted;
     private final Map<Long, Boolean> mTokenActiveStateMap = new HashMap<>();
     private String mDeviceName;
     private String mDeviceId;
@@ -110,8 +108,9 @@
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({ENROLLMENT_STATE_NONE, ENROLLMENT_STATE_UNIQUE_ID,
-                ENROLLMENT_STATE_ENCRYPTION_COMPLETED, ENROLLMENT_STATE_HANDLE})
-    public @interface EnrollmentState {}
+            ENROLLMENT_STATE_ENCRYPTION_COMPLETED, ENROLLMENT_STATE_HANDLE})
+    public @interface EnrollmentState {
+    }
 
     @EnrollmentState
     private int mEnrollmentState;
@@ -190,7 +189,8 @@
     @Override
     public void enrollmentHandshakeAccepted(BluetoothDevice device) {
         addEnrollmentServiceLog("enrollmentHandshakeAccepted");
-        mCarTrustAgentBleManager.sendMessage(device, CONFIRMATION_SIGNAL);
+        mCarTrustAgentBleManager.sendMessage(device, CONFIRMATION_SIGNAL,
+                OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
         setEnrollmentHandshakeAccepted(true);
     }
 
@@ -219,7 +219,7 @@
         }
     }
 
-    /**
+    /*
      * Returns if there is an active token for the given user and handle.
      *
      * @param handle handle corresponding to the escrow token
@@ -416,7 +416,8 @@
                 Log.d(TAG, "Sending handle: " + handle);
             }
             mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
-                    mEncryptionKey.encryptData(Utils.longToBytes(handle)));
+                    mEncryptionKey.encryptData(Utils.longToBytes(handle)),
+                    OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ true);
         } else {
             removeEscrowToken(handle, uid);
         }
@@ -481,7 +482,6 @@
             }
             return;
         }
-
         switch (mEnrollmentState) {
             case ENROLLMENT_STATE_NONE:
                 notifyDeviceIdReceived(value);
@@ -523,8 +523,8 @@
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "Sending device id: " + uniqueId.toString());
         }
-        mCarTrustAgentBleManager.sendMessage(
-                mRemoteEnrollmentDevice, Utils.uuidToBytes(uniqueId));
+        mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice, Utils.uuidToBytes(uniqueId),
+                OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ false);
         mEnrollmentState++;
     }
 
@@ -541,13 +541,16 @@
     /**
      * Processes the given message as one that will establish encryption for secure communication.
      *
-     * <p>This method should be called continually until {@link #mEnrollmentHandshakeAccepted} is
-     * {@code true}, meaning an secure channel has been set up.
+     * <p>This method should be called continually until {@link #mEncryptionState} is
+     * {@link HandshakeState#FINISHED}, meaning an secure channel has been set up.
      *
      * @param message The message received from the connected device.
      * @throws HandshakeException If an error was encountered during the handshake flow.
      */
     private void processInitEncryptionMessage(byte[] message) throws HandshakeException {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Processing init encryption message.");
+        }
         switch (mEncryptionState) {
             case HandshakeState.UNKNOWN:
                 if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -557,7 +560,8 @@
                 mHandshakeMessage = mEncryptionRunner.respondToInitRequest(message);
                 mEncryptionState = mHandshakeMessage.getHandshakeState();
                 mCarTrustAgentBleManager.sendMessage(
-                        mRemoteEnrollmentDevice, mHandshakeMessage.getNextMessage());
+                        mRemoteEnrollmentDevice, mHandshakeMessage.getNextMessage(),
+                        OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
 
                 if (Log.isLoggable(TAG, Log.DEBUG)) {
                     Log.d(TAG, "Updated encryption state: " + mEncryptionState);
@@ -582,9 +586,9 @@
                     showVerificationCode();
                     return;
                 }
-
                 mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
-                        mHandshakeMessage.getNextMessage());
+                        mHandshakeMessage.getNextMessage(), OperationType.ENCRYPTION_HANDSHAKE,
+                        /* isPayloadEncrypted= */ false);
                 break;
             case HandshakeState.VERIFICATION_NEEDED:
                 Log.w(TAG, "Encountered VERIFICATION_NEEDED state when it should have been "
@@ -638,8 +642,6 @@
     }
 
     private synchronized void setEnrollmentHandshakeAccepted(boolean accepted) {
-        mEnrollmentHandshakeAccepted = accepted;
-
         if (!accepted) {
             return;
         }
@@ -846,7 +848,7 @@
      * <p>
      * It also registers for death notifications of the host.
      */
-    private class EnrollmentStateClient implements IBinder.DeathRecipient {
+    private class EnrollmentStateClient implements DeathRecipient {
         private final IBinder mListenerBinder;
         private final ICarTrustAgentEnrollmentCallback mListener;
 
@@ -877,7 +879,7 @@
         }
     }
 
-    private class BleStateChangeClient implements IBinder.DeathRecipient {
+    private class BleStateChangeClient implements DeathRecipient {
         private final IBinder mListenerBinder;
         private final ICarTrustAgentBleCallback mListener;
 
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java
index ea3bc1b..cebd79e 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java
@@ -92,7 +92,6 @@
             .addUid(mParent.getAppEntry().info.uid)
             .setRetrieveDetail(false)
             .setNetworkTemplate(template)
-            .setSubscriberId(template.getSubscriberId())
             .build();
     }
 
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java
index 27aa4bd..9a2cdbf 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java
@@ -393,8 +393,7 @@
                 final NetworkCycleDataForUidLoader.Builder builder
                     = NetworkCycleDataForUidLoader.builder(mContext);
                 builder.setRetrieveDetail(true)
-                    .setNetworkTemplate(mTemplate)
-                    .setSubscriberId(mTemplate.getSubscriberId());
+                    .setNetworkTemplate(mTemplate);
                 if (mAppItem.category == AppItem.CATEGORY_USER) {
                     for (int i = 0; i < mAppItem.uids.size(); i++) {
                         builder.addUid(mAppItem.uids.keyAt(i));
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java
index c6d3b4f..83e11f2 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java
@@ -492,7 +492,6 @@
         public Loader<List<NetworkCycleChartData>> onCreateLoader(int id, Bundle args) {
             return NetworkCycleChartDataLoader.builder(getContext())
                     .setNetworkTemplate(mTemplate)
-                    .setSubscriberId(mTelephonyManager.getSubscriberId(mSubId))
                     .build();
         }
 
@@ -518,8 +517,7 @@
             return new NetworkStatsSummaryLoader.Builder(getContext())
                     .setStartTime(mChart.getInspectStart())
                     .setEndTime(mChart.getInspectEnd())
-                    .setNetworkType(mNetworkType)
-                    .setSubscriberId(mTelephonyManager.getSubscriberId(mSubId))
+                    .setNetworkTemplate(mTemplate)
                     .build();
         }
 
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml b/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml
index 0f2a8e7..86d505c 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml
@@ -13,25 +13,19 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <LinearLayout
-        android:orientation="horizontal"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_weight="1" >
-        <TextView
-            android:id="@+id/bluetooth_device"
-            android:layout_height="wrap_content"
-            android:layout_width="wrap_content"/>
-        <Button
-            android:id="@+id/bluetooth_pick_device"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/bluetooth_pick_device"/>
-    </LinearLayout>
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent" 
+    android:columnCount = "2">
+    <TextView
+      android:id="@+id/bluetooth_device"
+      android:layout_height="wrap_content"
+      android:layout_width="wrap_content"/>
+    <Button
+      android:id="@+id/bluetooth_pick_device"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="@string/bluetooth_pick_device"/>
     <Button
         android:id="@+id/bluetooth_headset_connect"
         android:layout_width="wrap_content"
@@ -58,6 +52,21 @@
         android:layout_height="wrap_content"
         android:text="@string/bluetooth_quiet_mode_enable"/>
     <Button
+        android:id="@+id/bluetooth_start_outgoing_call"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/bluetooth_start_outgoing_call"/>
+    <EditText
+        android:id="@+id/bluetooth_outgoing_phone_number"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:hint="@string/bluetooth_outgoing_phone_number"/>
+     <Button
+        android:id="@+id/bluetooth_end_outgoing_call"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/bluetooth_end_outgoing_call"/>
+    <Button
         android:id="@+id/bluetooth_hold_call"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -72,4 +81,4 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/bluetooth_voice_recognition_disable"/>
-</LinearLayout>
+</GridLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index d292ee6..77b64c7 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -265,6 +265,9 @@
     <string name="bluetooth_pick_device" translatable="false">Pick Device</string>
     <string name="bluetooth_quiet_mode_enable" translatable="false">Quiet Mode</string>
     <string name="bluetooth_hold_call" translatable="false">Hold call</string>
+    <string name="bluetooth_start_outgoing_call" translatable="false">Start call</string>
+    <string name="bluetooth_end_outgoing_call" translatable="false">End call</string>
+    <string name="bluetooth_outgoing_phone_number" translatable="false">Enter number</string>
     <string name="bluetooth_voice_recognition_enable" translatable="false">Start Voice Recognition</string>
     <string name="bluetooth_voice_recognition_disable" translatable="false">Stop Voice Recognition</string>
     <string name="uploading_supported_feature" translatable="false">Uploading_Supported bit value</string>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java
index 2d57d28..619a2b3 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java
@@ -20,6 +20,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothDevicePicker;
 import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothHeadsetClientCall;
 import android.bluetooth.BluetoothProfile;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -31,7 +32,9 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
+import android.widget.EditText;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
@@ -53,8 +56,12 @@
     Button mHoldCall;
     Button mEnableBVRA;
     Button mDisableBVRA;
+    Button mStartOutgoingCall;
+    Button mEndOutgoingCall;
+    EditText mOutgoingPhoneNumber;
 
     BluetoothHeadsetClient mHfpClientProfile;
+    BluetoothHeadsetClientCall mOutgoingCall;
 
     // Intent for picking a Bluetooth device
     public static final String DEVICE_PICKER_ACTION =
@@ -75,6 +82,9 @@
         mHoldCall = (Button) v.findViewById(R.id.bluetooth_hold_call);
         mEnableBVRA = (Button) v.findViewById(R.id.bluetooth_voice_recognition_enable);
         mDisableBVRA = (Button) v.findViewById(R.id.bluetooth_voice_recognition_disable);
+        mStartOutgoingCall = (Button) v.findViewById(R.id.bluetooth_start_outgoing_call);
+        mEndOutgoingCall = (Button) v.findViewById(R.id.bluetooth_end_outgoing_call);
+        mOutgoingPhoneNumber = (EditText) v.findViewById(R.id.bluetooth_outgoing_phone_number);
 
         // Pick a bluetooth device
         mDevicePicker.setOnClickListener(new View.OnClickListener() {
@@ -147,6 +157,23 @@
                 stopBVRA();
             }
         });
+
+        // Start a outgoing call
+        mStartOutgoingCall.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                startCall();
+            }
+        });
+
+        // Stop a outgoing call
+        mEndOutgoingCall.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                stopCall();
+            }
+        });
+
         return v;
     }
 
@@ -260,6 +287,57 @@
         mHfpClientProfile.stopVoiceRecognition(mPickedDevice);
     }
 
+    void startCall() {
+        if (mPickedDevice == null) {
+            Log.w(TAG, "Device null when trying to start voice call!");
+            return;
+        }
+
+        // Check if we have the proxy and connect the device.
+        if (mHfpClientProfile == null) {
+            Log.w(TAG, "HFP Profile proxy not available, cannot start voice call to "
+                    + mPickedDevice);
+            return;
+        }
+
+        if (mOutgoingCall != null) {
+            Log.w(TAG, "Potential on-going call or a stale call " + mOutgoingCall);
+        }
+
+        String number = mOutgoingPhoneNumber.getText().toString();
+        mOutgoingCall = mHfpClientProfile.dial(mPickedDevice, number);
+        if (mOutgoingCall == null) {
+            Log.w(TAG, "Fail to dial number " + number + ". Make sure profile connect first.");
+        } else {
+            Log.d(TAG, "Succeed in creating outgoing call " + mOutgoingCall + " for number "
+                    + number);
+        }
+    }
+
+    void stopCall() {
+        if (mPickedDevice == null) {
+            Log.w(TAG, "Device null when trying to stop voice call!");
+            return;
+        }
+
+        // Check if we have the proxy and connect the device.
+        if (mHfpClientProfile == null) {
+            Log.w(TAG, "HFP Profile proxy not available, cannot stop voice call to "
+                    + mPickedDevice);
+            return;
+        }
+
+        if (mOutgoingCall != null) {
+            if (mHfpClientProfile.terminateCall(mPickedDevice, mOutgoingCall)) {
+                Log.d(TAG, "Succeed in terminating outgoing call " + mOutgoingCall);
+                mOutgoingCall = null;
+            } else {
+                Log.d(TAG, "Fail to terminate outgoing call " + mOutgoingCall);
+            }
+        } else {
+            Log.w(TAG, "No outgoing call to terminate");
+        }
+    }
 
 
     private final BroadcastReceiver mPickerReceiver = new BroadcastReceiver() {
@@ -271,6 +349,10 @@
 
             if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) {
                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                if (device == null) {
+                    Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show();
+                    return;
+                }
                 mPickedDevice = device;
                 String text = device.getName() == null ?
                     device.getAddress() : device.getName() + " " + device.getAddress();
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java
index ab48a82..68d1c73 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java
@@ -364,6 +364,10 @@
             if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) {
                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                 Log.v(TAG, "mPickerReceiver got " + device);
+                if (device == null) {
+                    Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show();
+                    return;
+                }
                 mMapProfile.connect(device);
 
                 // The receiver can now be disabled.
diff --git a/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java b/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java
index 993979a..02a3f13 100644
--- a/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java
+++ b/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
@@ -35,11 +36,9 @@
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.sysprop.CarProperties;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -77,6 +76,8 @@
     private ActivityManager mActivityManager;
     @Mock
     private CarUserManagerHelper.OnUsersUpdateListener mTestListener;
+    @Mock
+    private TestableFrameworkWrapper mTestableFrameworkWrapper;
 
     private static final String GUEST_USER_NAME = "testGuest";
     private static final String TEST_USER_NAME = "testUser";
@@ -98,7 +99,7 @@
         doReturn(InstrumentationRegistry.getTargetContext().getContentResolver())
                 .when(mContext).getContentResolver();
         doReturn(mContext).when(mContext).getApplicationContext();
-        mCarUserManagerHelper = new CarUserManagerHelper(mContext);
+        mCarUserManagerHelper = new CarUserManagerHelper(mContext, mTestableFrameworkWrapper);
         mCarUserManagerHelper.setDefaultAdminName(DEFAULT_ADMIN_NAME);
 
         mCurrentProcessUser = createUserInfoForId(UserHandle.myUserId());
@@ -111,8 +112,9 @@
         mForegroundUserId = ActivityManager.getCurrentUser();
         mForegroundUser = createUserInfoForId(mForegroundUserId);
 
-        // Clear boot override for every test
-        CarProperties.boot_user_override_id(null);
+        // Clear boot override for every test by returning the default value passed to the method
+        when(mTestableFrameworkWrapper.getBootUserOverrideId(anyInt()))
+                .thenAnswer(stub -> stub.getArguments()[0]);
     }
 
     @Test
@@ -164,9 +166,9 @@
         UserInfo user2 = createUserInfoForId(mForegroundUserId + 1);
         // Create two ephemeral users.
         UserInfo user3 = new UserInfo(
-              /* id= */mForegroundUserId + 2, /* name = */ "user3", UserInfo.FLAG_EPHEMERAL);
+                /* id= */mForegroundUserId + 2, /* name = */ "user3", UserInfo.FLAG_EPHEMERAL);
         UserInfo user4 = new UserInfo(
-              /* id= */mForegroundUserId + 3, /* name = */ "user4", UserInfo.FLAG_EPHEMERAL);
+                /* id= */mForegroundUserId + 3, /* name = */ "user4", UserInfo.FLAG_EPHEMERAL);
 
         mockGetUsers(user1, user2, user3, user4);
 
@@ -217,33 +219,33 @@
     @Test
     public void testCurrentProcessCanAddUsers() {
         doReturn(false).when(mUserManager)
-            .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
+                .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
         assertThat(mCarUserManagerHelper.canCurrentProcessAddUsers()).isTrue();
 
         doReturn(true).when(mUserManager)
-            .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
+                .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
         assertThat(mCarUserManagerHelper.canCurrentProcessAddUsers()).isFalse();
     }
 
     @Test
     public void testCurrentProcessCanRemoveUsers() {
         doReturn(false).when(mUserManager)
-            .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
+                .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
         assertThat(mCarUserManagerHelper.canCurrentProcessRemoveUsers()).isTrue();
 
         doReturn(true).when(mUserManager)
-            .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
+                .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
         assertThat(mCarUserManagerHelper.canCurrentProcessRemoveUsers()).isFalse();
     }
 
     @Test
     public void testCurrentProcessCanSwitchUsers() {
         doReturn(false).when(mUserManager)
-            .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+                .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
         assertThat(mCarUserManagerHelper.canCurrentProcessSwitchUsers()).isTrue();
 
         doReturn(true).when(mUserManager)
-            .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+                .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
         assertThat(mCarUserManagerHelper.canCurrentProcessSwitchUsers()).isFalse();
     }
 
@@ -277,7 +279,7 @@
 
     @Test
     public void testGetMaxSupportedUsers() {
-        SystemProperties.set("fw.max_users", "11");
+        setMaxSupportedUsers(11);
 
         // Max users - headless system user.
         assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(10);
@@ -285,7 +287,7 @@
 
     @Test
     public void testGetMaxSupportedRealUsers() {
-        SystemProperties.set("fw.max_users", "7");
+        setMaxSupportedUsers(7);
 
         // Create three managed profiles, and two normal users.
         UserInfo user1 = createUserInfoForId(10);
@@ -314,16 +316,16 @@
 
         mockGetUsers(mSystemUser, user1, user2, user3, user4);
 
-        SystemProperties.set("fw.max_users", "6");
+        setMaxSupportedUsers(6);
         assertThat(mCarUserManagerHelper.isUserLimitReached()).isFalse();
 
-        SystemProperties.set("fw.max_users", "5");
+        setMaxSupportedUsers(5);
         assertThat(mCarUserManagerHelper.isUserLimitReached()).isTrue();
     }
 
     @Test
     public void testIsUserLimitReachedIgnoresGuests() {
-        SystemProperties.set("fw.max_users", "6");
+        setMaxSupportedUsers(6);
 
         UserInfo user1 = createUserInfoForId(10);
         UserInfo user2 =
@@ -500,7 +502,7 @@
         mockGetUsers(adminInfo);
 
         assertThat(mCarUserManagerHelper.removeUser(adminInfo, GUEST_USER_NAME))
-            .isEqualTo(false);
+                .isEqualTo(false);
     }
 
     @Test
@@ -679,7 +681,7 @@
         assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS)).isTrue();
         assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_INSTALL_APPS)).isTrue();
         assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES))
-            .isTrue();
+                .isTrue();
         assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS)).isTrue();
     }
 
@@ -954,7 +956,7 @@
 
     private void mockGetUsers(UserInfo... users) {
         List<UserInfo> testUsers = new ArrayList<>();
-        for (UserInfo user: users) {
+        for (UserInfo user : users) {
             testUsers.add(user);
         }
         doReturn(testUsers).when(mUserManager).getUsers(true);
@@ -966,6 +968,11 @@
     }
 
     private void setDefaultBootUserOverride(int userId) {
-        CarProperties.boot_user_override_id(userId);
+        doReturn(userId).when(mTestableFrameworkWrapper)
+                .getBootUserOverrideId(anyInt());
     }
-}
+
+    private void setMaxSupportedUsers(int maxValue) {
+        doReturn(maxValue).when(mTestableFrameworkWrapper).userManagerGetMaxSupportedUsers();
+    }
+}
\ No newline at end of file
diff --git a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
index cfd50cd..a46cf37 100644
--- a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
@@ -99,6 +99,7 @@
     private final Context mContext;
     private final UserManager mUserManager;
     private final ActivityManager mActivityManager;
+    private final TestableFrameworkWrapper mTestableFrameworkWrapper;
     private String mDefaultAdminName;
     private Bitmap mDefaultGuestUserIcon;
     private ArrayList<OnUsersUpdateListener> mUpdateListeners;
@@ -122,10 +123,16 @@
      * @param context Application Context
      */
     public CarUserManagerHelper(Context context) {
+        this(context, new TestableFrameworkWrapper());
+    }
+
+    @VisibleForTesting
+    CarUserManagerHelper(Context context, TestableFrameworkWrapper testableFrameworkWrapper) {
         mUpdateListeners = new ArrayList<>();
         mContext = context.getApplicationContext();
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+        mTestableFrameworkWrapper = testableFrameworkWrapper;
     }
 
     /**
@@ -212,7 +219,7 @@
      *
      * This method checks for the initial user via three mechanisms in this order:
      * <ol>
-     *     <li>Check for a boot user override via {@link KEY_BOOT_USER_OVERRIDE_ID}</li>
+     *     <li>Check for a boot user override via {@link CarProperties#boot_user_override_id()}</li>
      *     <li>Check for the last active user in the system</li>
      *     <li>Fallback to the smallest user id that is not {@link UserHandle.USER_SYSTEM}</li>
      * </ol>
@@ -226,7 +233,7 @@
     public int getInitialUser() {
         List<Integer> allUsers = userInfoListToUserIdList(getAllPersistentUsers());
 
-        int bootUserOverride = CarProperties.boot_user_override_id().orElse(BOOT_USER_NOT_FOUND);
+        int bootUserOverride = mTestableFrameworkWrapper.getBootUserOverrideId(BOOT_USER_NOT_FOUND);
 
         // If an override user is present and a real user, return it
         if (bootUserOverride != BOOT_USER_NOT_FOUND
@@ -468,9 +475,9 @@
      */
     public int getMaxSupportedUsers() {
         if (isHeadlessSystemUser()) {
-            return UserManager.getMaxSupportedUsers() - 1;
+            return mTestableFrameworkWrapper.userManagerGetMaxSupportedUsers() - 1;
         }
-        return UserManager.getMaxSupportedUsers();
+        return mTestableFrameworkWrapper.userManagerGetMaxSupportedUsers();
     }
 
     /**
diff --git a/user/car-user-lib/src/android/car/userlib/TestableFrameworkWrapper.java b/user/car-user-lib/src/android/car/userlib/TestableFrameworkWrapper.java
new file mode 100644
index 0000000..472ccac
--- /dev/null
+++ b/user/car-user-lib/src/android/car/userlib/TestableFrameworkWrapper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.car.userlib;
+
+import android.os.UserManager;
+import android.sysprop.CarProperties;
+
+/**
+ * Testable versions of APIs from the framework.  This helps test difficult areas of the framework
+ * due to limitations like SELinux or hard to mock static methods.
+ */
+public class TestableFrameworkWrapper {
+    /**
+     * Wrapper around {@link CarProperties#boot_user_override_id()}
+     */
+    public int getBootUserOverrideId(int defaultValue) {
+        return CarProperties.boot_user_override_id().orElse(defaultValue);
+    }
+
+    /**
+     * Wrapper around {@link UserManager#getMaxSupportedUsers()}
+     */
+    public int userManagerGetMaxSupportedUsers() {
+        return UserManager.getMaxSupportedUsers();
+    }
+}