Merge "Add multi-user and user-switch support to VMS publisher binding."
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index e1ef1fe..41b6815 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -60,8 +60,11 @@
     </string-array>
     <!-- Default home activity -->
     <string name="defaultHomeActivity"><!--com.your.package/com.your.package.Activity--></string>
-    <!--  The com.android.car.VmsPublisherService will bind to this list of clients -->
-    <string-array translatable="false" name="vmsPublisherClients">
+    <!--  The com.android.car.vms.VmsClientManager will bind to this list of clients running as system user -->
+    <string-array translatable="false" name="vmsPublisherSystemClients">
+    </string-array>
+    <!--  The com.android.car.vms.VmsClientManager will bind to this list of clients running as current user -->
+    <string-array translatable="false" name="vmsPublisherUserClients">
     </string-array>
     <!-- Number of milliseconds to wait before trying re-bind to a crashed publisher. -->
     <integer name="millisecondsBeforeRebindToVmsPublisher">10000</integer>
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 63ea2a7..e2176d3 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -44,6 +44,7 @@
 import com.android.car.systeminterface.SystemInterface;
 import com.android.car.trust.CarTrustAgentEnrollmentService;
 import com.android.car.user.CarUserService;
+import com.android.car.vms.VmsClientManager;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.car.ICarServiceHelper;
 
@@ -88,8 +89,9 @@
 
     private final CarUserManagerHelper mUserManagerHelper;
     private CarUserService mCarUserService;
-    private VmsSubscriberService mVmsSubscriberService;
-    private VmsPublisherService mVmsPublisherService;
+    private final VmsClientManager mVmsClientManager;
+    private final VmsSubscriberService mVmsSubscriberService;
+    private final VmsPublisherService mVmsPublisherService;
 
     private final CarServiceBase[] mAllServices;
 
@@ -138,8 +140,10 @@
                 mAppFocusService, mCarInputService);
         mSystemStateControllerService = new SystemStateControllerService(
                 serviceContext, mCarAudioService, this);
+        mVmsClientManager = new VmsClientManager(serviceContext, mUserManagerHelper);
         mVmsSubscriberService = new VmsSubscriberService(serviceContext, mHal.getVmsHal());
-        mVmsPublisherService = new VmsPublisherService(serviceContext, mHal.getVmsHal());
+        mVmsPublisherService = new VmsPublisherService(serviceContext, mVmsClientManager,
+                mHal.getVmsHal());
         mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());
         mCarStorageMonitoringService = new CarStorageMonitoringService(serviceContext,
                 systemInterface);
@@ -170,6 +174,7 @@
         allServices.add(mCarDiagnosticService);
         allServices.add(mCarStorageMonitoringService);
         allServices.add(mCarConfigurationService);
+        allServices.add(mVmsClientManager);
         allServices.add(mVmsSubscriberService);
         allServices.add(mVmsPublisherService);
         allServices.add(mCarTrustAgentEnrollmentService);
diff --git a/service/src/com/android/car/VmsPublisherService.java b/service/src/com/android/car/VmsPublisherService.java
index 9ffecf0..6bc8d3e 100644
--- a/service/src/com/android/car/VmsPublisherService.java
+++ b/service/src/com/android/car/VmsPublisherService.java
@@ -22,26 +22,21 @@
 import android.car.vms.VmsLayer;
 import android.car.vms.VmsLayersOffering;
 import android.car.vms.VmsSubscriptionState;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.RemoteException;
-import android.os.UserHandle;
-import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 
 import com.android.car.hal.VmsHalService;
 import com.android.car.hal.VmsHalService.VmsHalPublisherListener;
+import com.android.car.vms.VmsClientManager;
 
 import java.io.PrintWriter;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
@@ -57,94 +52,40 @@
     private static final int MSG_HAL_SUBSCRIPTION_CHANGED = 1;
 
     private final Context mContext;
+    private final VmsClientManager mClientManager;
+    private final VmsListener mClientListener = new VmsListener();
     private final VmsHalService mHal;
-    private final Map<String, PublisherConnection> mPublisherConnectionMap = new ArrayMap<>();
-    private final Map<String, IVmsPublisherClient> mPublisherMap = new ArrayMap<>();
-    private final Handler mHandler = new EventHandler();
     private final VmsHalPublisherListener mHalPublisherListener;
+    private final Map<String, IVmsPublisherClient> mPublisherMap = Collections.synchronizedMap(
+            new ArrayMap<>());
+    private final Handler mHandler = new EventHandler();
 
-    private BroadcastReceiver mBootCompleteReceiver;
-
-    public VmsPublisherService(Context context, VmsHalService hal) {
+    public VmsPublisherService(Context context, VmsClientManager clientManager, VmsHalService hal) {
         mContext = context;
+        mClientManager = clientManager;
         mHal = hal;
-
         mHalPublisherListener = subscriptionState -> mHandler.sendMessage(
                 mHandler.obtainMessage(MSG_HAL_SUBSCRIPTION_CHANGED, subscriptionState));
     }
 
-    // Implements CarServiceBase interface.
     @Override
     public void init() {
+        mClientManager.registerConnectionListener(mClientListener);
         mHal.addPublisherListener(mHalPublisherListener);
-
-        if (isTestEnvironment()) {
-            Log.d(TAG, "Running under test environment");
-            bindToAllPublishers();
-        } else {
-            mBootCompleteReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(intent.getAction())) {
-                        onLockedBootCompleted();
-                    } else {
-                        Log.e(TAG, "Unexpected action received: " + intent);
-                    }
-                }
-            };
-
-            mContext.registerReceiver(mBootCompleteReceiver,
-                    new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED));
-        }
-        // Signal to publishers that the PublisherService is ready.
         mHal.signalPublisherServiceIsReady();
     }
 
-    private void bindToAllPublishers() {
-        String[] publisherNames = mContext.getResources().getStringArray(
-                R.array.vmsPublisherClients);
-        if (DBG) Log.d(TAG, "Publishers found: " + publisherNames.length);
-
-        for (String publisherName : publisherNames) {
-            if (TextUtils.isEmpty(publisherName)) {
-                Log.e(TAG, "empty publisher name");
-                continue;
-            }
-            ComponentName name = ComponentName.unflattenFromString(publisherName);
-            if (name == null) {
-                Log.e(TAG, "invalid publisher name: " + publisherName);
-                continue;
-            }
-
-            if (!mContext.getPackageManager().isPackageAvailable(name.getPackageName())) {
-                Log.w(TAG, "VMS publisher not installed: " + publisherName);
-                continue;
-            }
-
-            bind(name);
-        }
-    }
-
     @Override
     public void release() {
-        if (mBootCompleteReceiver != null) {
-            mContext.unregisterReceiver(mBootCompleteReceiver);
-            mBootCompleteReceiver = null;
-        }
+        mClientManager.unregisterConnectionListener(mClientListener);
         mHal.removePublisherListener(mHalPublisherListener);
-
-        for (PublisherConnection connection : mPublisherConnectionMap.values()) {
-            mContext.unbindService(connection);
-        }
-        mPublisherConnectionMap.clear();
         mPublisherMap.clear();
     }
 
     @Override
     public void dump(PrintWriter writer) {
         writer.println("*" + getClass().getSimpleName() + "*");
-        writer.println("mPublisherMap:" + mPublisherMap);
-        writer.println("mPublisherConnectionMap:" + mPublisherConnectionMap);
+        writer.println("mPublisherMap:" + mPublisherMap.keySet());
     }
 
     /* Called in arbitrary binder thread */
@@ -199,12 +140,6 @@
         return mHal.getPublisherId(publisherInfo);
     }
 
-    private void onLockedBootCompleted() {
-        if (DBG) Log.i(TAG, "onLockedBootCompleted");
-
-        bindToAllPublishers();
-    }
-
     /**
      * This method is only invoked by VmsHalService.notifyPublishers which is synchronized.
      * Therefore this method only sees a non-decreasing sequence.
@@ -222,110 +157,30 @@
         }
     }
 
-    /**
-     * Tries to bind to a publisher.
-     *
-     * @param name publisher component name (e.g. android.car.vms.logger/.LoggingService).
-     */
-    private void bind(ComponentName name) {
-        String publisherName = name.flattenToString();
-        if (DBG) {
-            Log.d(TAG, "binding to: " + publisherName);
-        }
-
-        if (mPublisherConnectionMap.containsKey(publisherName)) {
-            // Already registered, nothing to do.
-            return;
-        }
-        Intent intent = new Intent();
-        intent.setComponent(name);
-        PublisherConnection connection = new PublisherConnection(name);
-        if (mContext.bindServiceAsUser(intent, connection,
-                Context.BIND_AUTO_CREATE, UserHandle.SYSTEM)) {
-            mPublisherConnectionMap.put(publisherName, connection);
-        } else {
-            Log.e(TAG, "unable to bind to: " + publisherName);
-        }
-    }
-
-    /**
-     * Removes the publisher and associated connection.
-     *
-     * @param name publisher component name (e.g. android.car.vms.Logger).
-     */
-    private void unbind(ComponentName name) {
-        String publisherName = name.flattenToString();
-        if (DBG) {
-            Log.d(TAG, "unbinding from: " + publisherName);
-        }
-
-        boolean found = mPublisherMap.remove(publisherName) != null;
-        if (found) {
-            PublisherConnection connection = mPublisherConnectionMap.get(publisherName);
-            mContext.unbindService(connection);
-            mPublisherConnectionMap.remove(publisherName);
-        } else {
-            Log.e(TAG, "unbind: unknown publisher." + publisherName);
-        }
-    }
-
-    private boolean isTestEnvironment() {
-        // If the context has "test" in it.
-        return mContext.getBasePackageName().contains("test");
-    }
-
-    class PublisherConnection implements ServiceConnection {
-        private final IBinder mToken = new Binder();
-        private final ComponentName mName;
-
-        PublisherConnection(ComponentName name) {
-            mName = name;
-        }
-
-        private final Runnable mBindRunnable = new Runnable() {
-            @Override
-            public void run() {
-                Log.d(TAG, "delayed binding for: " + mName);
-                bind(mName);
-            }
-        };
-
+    private class VmsListener implements VmsClientManager.ConnectionListener {
         /**
-         * Once the service binds to a publisher service, the publisher binder is added to
-         * mPublisherMap
-         * and the publisher is configured to use this service.
+         * Once the manager binds to a publisher client, the client's binder is added to
+         * {@code mPublisherMap} and the client is configured to use this service.
          */
         @Override
-        public void onServiceConnected(ComponentName name, IBinder binder) {
-            if (DBG) {
-                Log.d(TAG, "onServiceConnected, name: " + name + ", binder: " + binder);
-            }
+        public void onClientConnected(String publisherName, IBinder binder) {
+            if (DBG) Log.d(TAG, "onClientConnected: " + publisherName);
             IVmsPublisherClient service = IVmsPublisherClient.Stub.asInterface(binder);
-            mPublisherMap.put(name.flattenToString(), service);
+            mPublisherMap.put(publisherName, service);
             try {
-                service.setVmsPublisherService(mToken, VmsPublisherService.this);
+                service.setVmsPublisherService(new Binder(), VmsPublisherService.this);
             } catch (RemoteException e) {
-                Log.e(TAG, "unable to configure publisher: " + name, e);
+                Log.e(TAG, "unable to configure publisher: " + publisherName, e);
             }
         }
 
         /**
-         * Tries to rebind to the publisher service.
+         * Removes disconnected clients from {@code mPublisherMap}.
          */
         @Override
-        public void onServiceDisconnected(ComponentName name) {
-            String publisherName = name.flattenToString();
-            Log.d(TAG, "onServiceDisconnected, name: " + publisherName);
-
-            int millisecondsToWait = mContext.getResources().getInteger(
-                    com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
-            if (!mName.flattenToString().equals(name.flattenToString())) {
-                throw new IllegalArgumentException(
-                    "Mismatch on publisherConnection. Expected: " + mName + " Got: " + name);
-            }
-            mHandler.postDelayed(mBindRunnable, millisecondsToWait);
-
-            unbind(name);
+        public void onClientDisconnected(String publisherName) {
+            if (DBG) Log.d(TAG, "onClientDisconnected: " + publisherName);
+            mPublisherMap.remove(publisherName);
         }
     }
 
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
new file mode 100644
index 0000000..c341b7a
--- /dev/null
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -0,0 +1,353 @@
+/*
+ * 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.vms;
+
+import android.car.userlib.CarUserManagerHelper;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.UserInfo;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.car.CarServiceBase;
+import com.android.car.R;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * Manages service connections lifecycle for VMS publisher clients.
+ *
+ * Binds to system-level clients at boot and creates/destroys bindings for userspace clients
+ * according to the Android user lifecycle.
+ */
+public class VmsClientManager implements CarServiceBase {
+    private static final boolean DBG = false;
+    private static final String TAG = "VmsClientManager";
+
+    /**
+     * Interface for receiving updates about client connections.
+     */
+    public interface ConnectionListener {
+        /**
+         * Called when a client connection is established or re-established.
+         *
+         * @param clientName String that uniquely identifies the service and user.
+         * @param binder Binder for communicating with the client.
+         */
+        void onClientConnected(String clientName, IBinder binder);
+
+        /**
+         * Called when a client connection is terminated.
+         *
+         * @param clientName String that uniquely identifies the service and user.
+         */
+        void onClientDisconnected(String clientName);
+    }
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final CarUserManagerHelper mUserManagerHelper;
+    private final int mMillisBeforeRebind;
+
+    @GuardedBy("mListeners")
+    private final ArrayList<ConnectionListener> mListeners = new ArrayList<>();
+    @GuardedBy("mSystemClients")
+    private final Map<String, ClientConnection> mSystemClients = new ArrayMap<>();
+    @GuardedBy("mCurrentUserClients")
+    private final Map<String, ClientConnection> mCurrentUserClients = new ArrayMap<>();
+    @GuardedBy("mCurrentUserClients")
+    private int mCurrentUser;
+
+    final BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_LOCKED_BOOT_COMPLETED:
+                    bindToSystemClients();
+                    break;
+                default:
+                    Log.e(TAG, "Unexpected intent received: " + intent);
+            }
+        }
+    };
+
+    final BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DBG) Log.d(TAG, "Received " + intent);
+            switch (intent.getAction()) {
+                case Intent.ACTION_USER_SWITCHED:
+                case Intent.ACTION_USER_UNLOCKED:
+                    bindToCurrentUserClients();
+                    break;
+                default:
+                    Log.e(TAG, "Unexpected intent received: " + intent);
+            }
+        }
+    };
+
+    /**
+     * Constructor for client managers.
+     *
+     * @param context Context to use for registering receivers and binding services.
+     * @param userManagerHelper User manager for querying current user state.
+     */
+    public VmsClientManager(Context context, CarUserManagerHelper userManagerHelper) {
+        mContext = context;
+        mHandler = new Handler(Looper.getMainLooper());
+        mUserManagerHelper = userManagerHelper;
+        mMillisBeforeRebind = mContext.getResources().getInteger(
+                com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
+    }
+
+    @Override
+    public void init() {
+        mContext.registerReceiver(mBootCompletedReceiver,
+                new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED));
+
+        IntentFilter userSwitchFilter = new IntentFilter();
+        userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
+        userSwitchFilter.addAction(Intent.ACTION_USER_UNLOCKED);
+        mContext.registerReceiverAsUser(mUserSwitchReceiver, UserHandle.ALL, userSwitchFilter, null,
+                null);
+    }
+
+    @Override
+    public void release() {
+        mContext.unregisterReceiver(mBootCompletedReceiver);
+        mContext.unregisterReceiver(mUserSwitchReceiver);
+        synchronized (mSystemClients) {
+            unbind(mSystemClients);
+        }
+        synchronized (mCurrentUserClients) {
+            unbind(mCurrentUserClients);
+        }
+    }
+
+    @Override
+    public void dump(PrintWriter writer) {
+        writer.println("*" + getClass().getSimpleName() + "*");
+        writer.println("mListeners:" + mListeners);
+        writer.println("mSystemClients:" + mSystemClients.keySet());
+        writer.println("mCurrentUser:" + mCurrentUser);
+        writer.println("mCurrentUserClients:" + mCurrentUserClients.keySet());
+    }
+
+    /**
+     * Registers a new client connection state listener.
+     *
+     * @param listener Listener to register.
+     */
+    public void registerConnectionListener(ConnectionListener listener) {
+        synchronized (mListeners) {
+            if (!mListeners.contains(listener)) {
+                mListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Unregisters a client connection state listener.
+     *
+     * @param listener Listener to remove.
+     */
+    public void unregisterConnectionListener(ConnectionListener listener) {
+        synchronized (mListeners) {
+            mListeners.remove(listener);
+        }
+    }
+
+    private void bindToSystemClients() {
+        String[] clientNames = mContext.getResources().getStringArray(
+                R.array.vmsPublisherSystemClients);
+        Log.i(TAG, "Attempting to bind " + clientNames.length + " system client(s)");
+        synchronized (mSystemClients) {
+            for (String clientName : clientNames) {
+                bind(mSystemClients, clientName, UserHandle.SYSTEM);
+            }
+        }
+    }
+
+    private void bindToCurrentUserClients() {
+        UserInfo userInfo = mUserManagerHelper.getCurrentForegroundUserInfo();
+        synchronized (mCurrentUserClients) {
+            if (mCurrentUser != userInfo.id) {
+                unbind(mCurrentUserClients);
+            }
+            mCurrentUser = userInfo.id;
+
+            // To avoid the risk of double-binding, clients running as the system user must only
+            // ever be bound in bindToSystemClients().
+            // In a headless multi-user system, the system user will never be in the foreground.
+            if (mCurrentUser == UserHandle.USER_SYSTEM) {
+                Log.e(TAG, "System user in foreground. Userspace clients will not be bound.");
+                return;
+            }
+
+            String[] clientNames = mContext.getResources().getStringArray(
+                    R.array.vmsPublisherUserClients);
+            Log.i(TAG, "Attempting to bind " + clientNames.length + " user client(s)");
+            for (String clientName : clientNames) {
+                bind(mCurrentUserClients, clientName, userInfo.getUserHandle());
+            }
+        }
+    }
+
+    private void bind(Map<String, ClientConnection> connectionMap, String clientName,
+            UserHandle userHandle) {
+        if (connectionMap.containsKey(clientName)) {
+            return;
+        }
+
+        ComponentName name = ComponentName.unflattenFromString(clientName);
+        if (name == null) {
+            Log.e(TAG, "Invalid client name: " + clientName);
+            return;
+        }
+
+        if (!mContext.getPackageManager().isPackageAvailable(name.getPackageName())) {
+            Log.w(TAG, "Client not installed: " + clientName);
+            return;
+        }
+
+        ClientConnection connection = new ClientConnection(name, userHandle);
+        if (connection.bind()) {
+            Log.i(TAG, "Client bound: " + connection);
+            connectionMap.put(clientName, connection);
+        } else {
+            Log.w(TAG, "Binding failed: " + connection);
+        }
+    }
+
+    private void unbind(Map<String, ClientConnection> connectionMap) {
+        for (ClientConnection connection : connectionMap.values()) {
+            connection.unbind();
+        }
+        connectionMap.clear();
+    }
+
+    private void notifyListenersOnClientConnected(String clientName, IBinder binder) {
+        synchronized (mListeners) {
+            for (ConnectionListener listener : mListeners) {
+                listener.onClientConnected(clientName, binder);
+            }
+        }
+    }
+
+    private void notifyListenersOnClientDisconnected(String clientName) {
+        synchronized (mListeners) {
+            for (ConnectionListener listener : mListeners) {
+                listener.onClientDisconnected(clientName);
+            }
+        }
+    }
+
+    class ClientConnection implements ServiceConnection {
+        private final ComponentName mName;
+        private final UserHandle mUser;
+        private final String mFullName;
+        private boolean mIsBound = false;
+        private IBinder mBinder;
+
+        ClientConnection(ComponentName name, UserHandle user) {
+            mName = name;
+            mUser = user;
+            mFullName = mName.flattenToString() + " U=" + mUser.getIdentifier();
+        }
+
+        synchronized boolean bind() {
+            // Ignore if already bound
+            if (mIsBound) {
+                return true;
+            }
+
+            if (DBG) Log.d(TAG, "binding: " + mFullName);
+            Intent intent = new Intent();
+            intent.setComponent(mName);
+            try {
+                mIsBound = mContext.bindServiceAsUser(intent, this, Context.BIND_AUTO_CREATE,
+                        mHandler, mUser);
+            } catch (SecurityException e) {
+                Log.e(TAG, "While binding " + mFullName, e);
+            }
+
+            return mIsBound;
+        }
+
+        synchronized void unbind() {
+            if (DBG) Log.d(TAG, "unbinding: " + mFullName);
+            try {
+                mContext.unbindService(this);
+            } catch (Throwable t) {
+                Log.e(TAG, "While unbinding " + mFullName, t);
+            }
+            mIsBound = false;
+            if (mBinder != null) {
+                notifyListenersOnClientDisconnected(mFullName);
+            }
+            mBinder = null;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder binder) {
+            if (DBG) Log.d(TAG, "onServiceConnected: " + mFullName);
+            mBinder = binder;
+            notifyListenersOnClientConnected(mFullName, mBinder);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (DBG) Log.d(TAG, "onServiceDisconnected: " + mFullName);
+            if (mBinder != null) {
+                notifyListenersOnClientDisconnected(mFullName);
+            }
+            mBinder = null;
+            // No need to unbind and rebind, per onServiceDisconnected documentation:
+            // "binding to the service will remain active, and you will receive a call
+            // to onServiceConnected when the Service is next running"
+        }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            if (DBG) Log.d(TAG, "onBindingDied: " + mFullName);
+            unbind();
+            mHandler.postDelayed(this::bind, mMillisBeforeRebind);
+        }
+
+        @Override
+        public void onNullBinding(ComponentName name) {
+            if (DBG) Log.d(TAG, "onNullBinding: " + mFullName);
+            unbind();
+        }
+
+        @Override
+        public String toString() {
+            return mFullName;
+        }
+    }
+}
diff --git a/tests/carservice_test/src/com/android/car/VmsPublisherClientServiceTest.java b/tests/carservice_test/src/com/android/car/VmsPublisherClientServiceTest.java
index a54b0e3..2f9d34e 100644
--- a/tests/carservice_test/src/com/android/car/VmsPublisherClientServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/VmsPublisherClientServiceTest.java
@@ -21,6 +21,7 @@
 
 import android.car.VehicleAreaType;
 import android.car.vms.VmsLayer;
+import android.content.Intent;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
@@ -28,6 +29,7 @@
 import android.hardware.automotive.vehicle.V2_0.VmsBaseMessageIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageType;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageWithLayerIntegerValuesIndex;
+import android.os.UserHandle;
 import android.util.Log;
 
 import androidx.test.filters.MediumTest;
@@ -74,8 +76,10 @@
 
     @Override
     protected synchronized void configureResourceOverrides(MockResources resources) {
-        resources.overrideResource(R.array.vmsPublisherClients,
+        resources.overrideResource(R.array.vmsPublisherSystemClients,
             new String[]{ getFlattenComponent(SimpleVmsPublisherClientService.class) });
+        resources.overrideResource(R.array.vmsPublisherUserClients,
+                new String[]{ getFlattenComponent(SimpleVmsPublisherClientService.class) });
     }
 
     private VehiclePropValue getHalSubscriptionRequest() {
@@ -96,6 +100,16 @@
         mHalHandlerSemaphore = new Semaphore(0);
         super.setUp();
 
+        // Trigger VmsClientManager to bind to the SimpleVmsPublisherClientService
+        if (getContext().getUserId() == UserHandle.USER_SYSTEM) {
+            // If test is running as U0, trigger system client binding
+            getContext().sendBroadcast(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
+        } else {
+            // If test is running as U10+, trigger user client binding
+            getContext().sendBroadcastAsUser(new Intent(Intent.ACTION_USER_SWITCHED),
+                    UserHandle.ALL);
+        }
+
         // Inject a subscribe event which simulates the HAL is subscribed to the Mock Publisher.
         MockedVehicleHal mHal = getMockedVehicleHal();
         mHal.injectEvent(getHalSubscriptionRequest());
@@ -164,4 +178,4 @@
             return mValue;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java b/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
index e5a592c..631d85a 100644
--- a/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
+++ b/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
@@ -25,9 +25,11 @@
 import android.car.vms.VmsAvailableLayers;
 import android.car.vms.VmsLayer;
 import android.car.vms.VmsSubscriberManager;
+import android.content.Intent;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyChangeMode;
+import android.os.UserHandle;
 
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
@@ -108,7 +110,11 @@
 
     @Override
     protected synchronized void configureResourceOverrides(MockResources resources) {
-        resources.overrideResource(com.android.car.R.array.vmsPublisherClients,
+        // Override publisher client endpoint configurations
+        // Both lists must be set, but only one will be used (see setUp)
+        resources.overrideResource(com.android.car.R.array.vmsPublisherSystemClients,
+                new String[]{getFlattenComponent(VmsPublisherClientMockService.class)});
+        resources.overrideResource(com.android.car.R.array.vmsPublisherUserClients,
                 new String[]{getFlattenComponent(VmsPublisherClientMockService.class)});
     }
 
@@ -136,6 +142,16 @@
         mClientCallback = new TestClientCallback();
         mVmsSubscriberManager.setVmsSubscriberClientCallback(mExecutor, mClientCallback);
         mVmsSubscriberManager.subscribe(LAYER);
+
+        // Trigger VmsClientManager to bind to the VmsPublisherClientMockService
+        if (getContext().getUserId() == UserHandle.USER_SYSTEM) {
+            // If test is running as U0, trigger system client binding
+            getContext().sendBroadcast(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
+        } else {
+            // If test is running as U10+, trigger user client binding
+            getContext().sendBroadcastAsUser(new Intent(Intent.ACTION_USER_SWITCHED),
+                    UserHandle.ALL);
+        }
     }
 
     public void postSetup() throws Exception {
@@ -151,6 +167,7 @@
     @Test
     public void testPublisherToSubscriber() throws Exception {
         postSetup();
+        assertTrue(mSubscriberSemaphore.tryAcquire(2L, TimeUnit.SECONDS));
         assertEquals(LAYER, mClientCallback.getLayer());
         assertTrue(Arrays.equals(PAYLOAD, mClientCallback.getPayload()));
     }
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
new file mode 100644
index 0000000..cd0e2a0
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
@@ -0,0 +1,643 @@
+/*
+ * 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.vms;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.car.userlib.CarUserManagerHelper;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VmsClientManagerTest {
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private CarUserManagerHelper mUserManager;
+    private UserInfo mUserInfo;
+
+    @Mock
+    private VmsClientManager.ConnectionListener mConnectionListener;
+    private VmsClientManager mClientManager;
+
+    @Captor
+    private ArgumentCaptor<ServiceConnection> mConnectionCaptor;
+
+    @Before
+    public void setUp() {
+        resetContext();
+        when(mPackageManager.isPackageAvailable(any())).thenReturn(true);
+
+        when(mResources.getInteger(
+                com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher)).thenReturn(
+                5);
+        when(mResources.getStringArray(
+                com.android.car.R.array.vmsPublisherSystemClients)).thenReturn(
+                        new String[]{
+                                "com.google.android.apps.vms.test/.VmsSystemClient"
+                        });
+        when(mResources.getStringArray(
+                com.android.car.R.array.vmsPublisherUserClients)).thenReturn(
+                        new String[]{
+                                "com.google.android.apps.vms.test/.VmsUserClient"
+                        });
+        mUserInfo = new UserInfo(10, "Driver", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+
+        mClientManager = new VmsClientManager(mContext, mUserManager);
+        mClientManager.registerConnectionListener(mConnectionListener);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        Thread.sleep(10); // Time to allow for delayed rebinds to settle
+        verify(mContext, atLeast(0)).getResources();
+        verify(mContext, atLeast(0)).getPackageManager();
+        verifyNoMoreInteractions(mContext);
+    }
+
+    @Test
+    public void testInit() {
+        mClientManager.init();
+
+        // Verify registration of boot completed receiver
+        ArgumentCaptor<IntentFilter> bootFilterCaptor = ArgumentCaptor.forClass(IntentFilter.class);
+        verify(mContext).registerReceiver(eq(mClientManager.mBootCompletedReceiver),
+                bootFilterCaptor.capture());
+        IntentFilter bootFilter = bootFilterCaptor.getValue();
+        assertEquals(1, bootFilter.countActions());
+        assertTrue(bootFilter.hasAction(Intent.ACTION_LOCKED_BOOT_COMPLETED));
+
+        // Verify registration of user switch receiver
+        ArgumentCaptor<IntentFilter> userFilterCaptor = ArgumentCaptor.forClass(IntentFilter.class);
+        verify(mContext).registerReceiverAsUser(eq(mClientManager.mUserSwitchReceiver),
+                eq(UserHandle.ALL), userFilterCaptor.capture(), isNull(), isNull());
+        IntentFilter userEventFilter = userFilterCaptor.getValue();
+        assertEquals(2, userEventFilter.countActions());
+        assertTrue(userEventFilter.hasAction(Intent.ACTION_USER_SWITCHED));
+        assertTrue(userEventFilter.hasAction(Intent.ACTION_USER_UNLOCKED));
+    }
+
+    @Test
+    public void testRelease() {
+        mClientManager.release();
+
+        // Verify both receivers are unregistered
+        verify(mContext).unregisterReceiver(mClientManager.mBootCompletedReceiver);
+        verify(mContext).unregisterReceiver(mClientManager.mUserSwitchReceiver);
+    }
+
+    @Test
+    public void testLockedBootCompleted() {
+        notifyLockedBootCompleted();
+        notifyLockedBootCompleted();
+
+        // Multiple events should only trigger a single bind, when successful
+        verifySystemBind(1);
+    }
+
+    @Test
+    public void testLockedBootCompleted_BindFailed() {
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenReturn(false);
+        notifyLockedBootCompleted();
+        notifyLockedBootCompleted();
+
+        // Failure state will trigger another attempt on event
+        verifySystemBind(2);
+    }
+
+    @Test
+    public void testLockedBootCompleted_BindException() {
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenThrow(
+                new SecurityException());
+        notifyLockedBootCompleted();
+        notifyLockedBootCompleted();
+
+        // Failure state will trigger another attempt on event
+        verifySystemBind(2);
+    }
+
+    @Test
+    public void testUserSwitched() {
+        notifyUserSwitched();
+        notifyUserSwitched();
+
+        // Multiple events should only trigger a single bind, when successful
+        verifyUserBind(1);
+    }
+
+    @Test
+    public void testUserSwitched_BindFailed() {
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenReturn(false);
+        notifyUserSwitched();
+        notifyUserSwitched();
+
+        // Failure state will trigger another attempt on event
+        verifyUserBind(2);
+    }
+
+    @Test
+    public void testUserSwitched_BindException() {
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenThrow(
+                new SecurityException());
+        notifyUserSwitched();
+        notifyUserSwitched();
+
+        // Failure state will trigger another attempt on event
+        verifyUserBind(2);
+    }
+
+    @Test
+    public void testUserUnlocked() {
+        notifyUserUnlocked();
+        notifyUserUnlocked();
+
+        // Multiple events should only trigger a single bind, when successful
+        verifyUserBind(1);
+    }
+
+    @Test
+    public void testUserUnlocked_BindFailed() {
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenReturn(false);
+        notifyUserUnlocked();
+        notifyUserUnlocked();
+
+        // Failure state will trigger another attempt on event
+        verifyUserBind(2);
+    }
+
+    @Test
+    public void testUserUnlocked_BindException() {
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenThrow(
+                new SecurityException());
+        notifyUserUnlocked();
+        notifyUserUnlocked();
+
+        // Failure state will trigger another attempt on event
+        verifyUserBind(2);
+    }
+
+    @Test
+    public void testUserSwitchedAndUnlocked() {
+        notifyUserSwitched();
+        notifyUserUnlocked();
+
+        // Multiple events should only trigger a single bind, when successful
+        verifyUserBind(1);
+    }
+
+    @Test
+    public void testUserSwitchedToSystemUser() {
+        mUserInfo = new UserInfo(UserHandle.USER_SYSTEM, "Owner", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+        notifyUserSwitched();
+
+        // System user should not trigger any binding
+        verifyUserBind(0);
+    }
+
+    @Test
+    public void testUnregisterConnectionListener() {
+        mClientManager.unregisterConnectionListener(mConnectionListener);
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        verifyZeroInteractions(mConnectionListener);
+    }
+
+    @Test
+    public void testOnSystemServiceConnected() {
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+        resetContext();
+
+        Binder binder = new Binder();
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, binder);
+
+        verify(mConnectionListener).onClientConnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsSystemClient U=0"),
+                eq(binder));
+    }
+
+    @Test
+    public void testOnUserServiceConnected() {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        resetContext();
+
+        Binder binder = new Binder();
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, binder);
+
+        verify(mConnectionListener).onClientConnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"),
+                eq(binder));
+    }
+
+    @Test
+    public void testOnSystemServiceDisconnected() throws Exception {
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onServiceDisconnected(null);
+
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsSystemClient U=0"));
+    }
+
+    @Test
+    public void testOnSystemServiceDisconnected_ServiceNotConnected() throws Exception {
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceDisconnected(null);
+
+        verifyZeroInteractions(mConnectionListener);
+    }
+
+    @Test
+    public void testOnUserServiceDisconnected() throws Exception {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onServiceDisconnected(null);
+
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"));
+    }
+
+    @Test
+    public void testOnUserServiceDisconnected_ServiceNotConnected() throws Exception {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceDisconnected(null);
+
+        verifyZeroInteractions(mConnectionListener);
+    }
+
+    @Test
+    public void testOnSystemServiceBindingDied() throws Exception {
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onBindingDied(null);
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsSystemClient U=0"));
+
+        Thread.sleep(10);
+        verifySystemBind(1);
+    }
+
+    @Test
+    public void testOnSystemServiceBindingDied_ServiceNotConnected() throws Exception {
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onBindingDied(null);
+
+        verify(mContext).unbindService(connection);
+        verifyZeroInteractions(mConnectionListener);
+
+        Thread.sleep(10);
+        verifySystemBind(1);
+    }
+
+    @Test
+    public void testOnUserServiceBindingDied() throws Exception {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onBindingDied(null);
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"));
+
+        Thread.sleep(10);
+        verifyUserBind(1);
+    }
+
+    @Test
+    public void testOnUserServiceBindingDied_ServiceNotConnected() throws Exception {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onBindingDied(null);
+
+        verify(mContext).unbindService(connection);
+        verifyZeroInteractions(mConnectionListener);
+
+        Thread.sleep(10);
+        verifyUserBind(1);
+    }
+
+    @Test
+    public void testOnSystemServiceNullBinding() throws Exception {
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onNullBinding(null);
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsSystemClient U=0"));
+    }
+
+    @Test
+    public void testOnSystemServiceNullBinding_ServiceNotConnected() throws Exception {
+        notifyLockedBootCompleted();
+        verifySystemBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onNullBinding(null);
+
+        verify(mContext).unbindService(connection);
+        verifyZeroInteractions(mConnectionListener);
+    }
+
+    @Test
+    public void testOnUserServiceNullBinding() throws Exception {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        connection.onNullBinding(null);
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"));
+    }
+
+    @Test
+    public void testOnUserServiceNullBinding_ServiceNotConnected() throws Exception {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        resetContext();
+
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onNullBinding(null);
+
+        verify(mContext).unbindService(connection);
+        verifyZeroInteractions(mConnectionListener);
+    }
+
+    @Test
+    public void testOnUserSwitched_UserChange() {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        resetContext();
+        reset(mConnectionListener);
+
+        mUserInfo = new UserInfo(11, "Driver", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+        notifyUserSwitched();
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"));
+        verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
+                mUserInfo.getUserHandle());
+    }
+
+    @Test
+    public void testOnUserSwitched_UserChange_ToSystemUser() {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        resetContext();
+        reset(mConnectionListener);
+
+        mUserInfo = new UserInfo(UserHandle.USER_SYSTEM, "Owner", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+        notifyUserSwitched();
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"));
+        // User processes will not be bound for system user
+        verifyBind(0, "com.google.android.apps.vms.test/.VmsUserClient",
+                mUserInfo.getUserHandle());
+    }
+
+    @Test
+    public void testOnUserSwitched_UserChange_ServiceNotConnected() {
+        notifyUserSwitched();
+        verifyUserBind(1);
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        resetContext();
+
+        mUserInfo = new UserInfo(11, "Driver", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+        notifyUserSwitched();
+
+        verify(mContext).unbindService(connection);
+        verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
+                mUserInfo.getUserHandle());
+    }
+
+    @Test
+    public void testOnUserUnlocked_UserChange() {
+        notifyUserUnlocked();
+        verifyUserBind(1);
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        resetContext();
+        reset(mConnectionListener);
+
+        mUserInfo = new UserInfo(11, "Driver", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+        notifyUserUnlocked();
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"));
+        verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
+                mUserInfo.getUserHandle());
+    }
+
+    @Test
+    public void testOnUserLocked_UserChange_ToSystemUser() {
+        notifyUserUnlocked();
+        verifyUserBind(1);
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        connection.onServiceConnected(null, new Binder());
+        resetContext();
+        reset(mConnectionListener);
+
+        mUserInfo = new UserInfo(UserHandle.USER_SYSTEM, "Owner", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+        notifyUserUnlocked();
+
+        verify(mContext).unbindService(connection);
+        verify(mConnectionListener).onClientDisconnected(
+                eq("com.google.android.apps.vms.test/com.google.android.apps.vms.test"
+                        + ".VmsUserClient U=10"));
+        // User processes will not be bound for system user
+        verifyBind(0, "com.google.android.apps.vms.test/.VmsUserClient",
+                mUserInfo.getUserHandle());
+    }
+
+    @Test
+    public void testOnUserUnlocked_UserChange_ServiceNotConnected() {
+        notifyUserUnlocked();
+        verifyUserBind(1);
+        ServiceConnection connection = mConnectionCaptor.getValue();
+        resetContext();
+
+        mUserInfo = new UserInfo(11, "Driver", 0);
+        when(mUserManager.getCurrentForegroundUserInfo()).thenReturn(mUserInfo);
+        notifyUserUnlocked();
+
+        verify(mContext).unbindService(connection);
+        verifyBind(1, "com.google.android.apps.vms.test/.VmsUserClient",
+                mUserInfo.getUserHandle());
+    }
+
+    private void resetContext() {
+        reset(mContext);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenReturn(true);
+        when(mContext.getResources()).thenReturn(mResources);
+    }
+
+    private void notifyLockedBootCompleted() {
+        mClientManager.mBootCompletedReceiver.onReceive(mContext,
+                new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
+    }
+
+    private void notifyUserSwitched() {
+        mClientManager.mUserSwitchReceiver.onReceive(mContext,
+                new Intent(Intent.ACTION_USER_SWITCHED));
+    }
+
+    private void notifyUserUnlocked() {
+        mClientManager.mUserSwitchReceiver.onReceive(mContext,
+                new Intent(Intent.ACTION_USER_UNLOCKED));
+    }
+
+    private void verifySystemBind(int times) {
+        verifyBind(times, "com.google.android.apps.vms.test/.VmsSystemClient",
+                UserHandle.SYSTEM);
+    }
+
+    private void verifyUserBind(int times) {
+        verifyBind(times, "com.google.android.apps.vms.test/.VmsUserClient",
+                mUserInfo.getUserHandle());
+    }
+
+    private void verifyBind(int times, String componentName,
+            UserHandle user) {
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext, times(times)).bindServiceAsUser(
+                intentCaptor.capture(),
+                mConnectionCaptor.capture(),
+                eq(Context.BIND_AUTO_CREATE), any(Handler.class), eq(user));
+        if (times > 0) {
+            assertEquals(
+                    ComponentName.unflattenFromString(componentName),
+                    intentCaptor.getValue().getComponent());
+        }
+    }
+}