Merge "Add a color to EmbeddedKitchenSinkApp"
diff --git a/car_product/overlay/frameworks/base/core/res/res/layout/resolver_different_item_header.xml b/car_product/overlay/frameworks/base/core/res/res/layout/resolver_different_item_header.xml
new file mode 100644
index 0000000..0874dde
--- /dev/null
+++ b/car_product/overlay/frameworks/base/core/res/res/layout/resolver_different_item_header.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 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.
+ */
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alwaysShow="true"
+ android:text="@string/use_a_different_app"
+ android:minHeight="56dp"
+ android:textAppearance="?attr/textAppearanceLarge"
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:elevation="8dp"
+/>
\ No newline at end of file
diff --git a/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list.xml b/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list.xml
index 9d64d65..d4cf11e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list.xml
@@ -20,7 +20,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
android:id="@id/contentPanel">
<LinearLayout
diff --git a/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list_with_default.xml b/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list_with_default.xml
index 62d4d69..6155465 100644
--- a/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list_with_default.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/layout/resolver_list_with_default.xml
@@ -20,7 +20,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:theme="@android:style/Theme.DeviceDefault"
android:maxCollapsedHeight="200dp"
android:id="@id/contentPanel">
@@ -146,7 +145,6 @@
android:layout_height="1dp"
android:background="?attr/dividerVertical"/>
-
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml b/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml
index 4f2e0fe..89d5716 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml
@@ -131,4 +131,56 @@
<style name="Theme.DeviceDefault.Light.DarkActionBar" parent="Theme.DeviceDefault"/>
<!-- DeviceDefault theme for the default system theme. -->
<style name="Theme.DeviceDefault.System" parent="Theme.DeviceDefault.Light.DarkActionBar" />
+
+ <!-- Theme used for the intent picker activity. -->
+ <style name="Theme.DeviceDefault.Resolver" parent="Theme.DeviceDefault">
+ <item name="windowEnterTransition">@empty</item>
+ <item name="windowExitTransition">@empty</item>
+ <item name="windowIsTranslucent">true</item>
+ <item name="windowNoTitle">true</item>
+ <item name="windowBackground">@color/transparent</item>
+ <item name="backgroundDimEnabled">true</item>
+ <item name="statusBarColor">@color/transparent</item>
+ <item name="windowContentOverlay">@null</item>
+ <item name="colorControlActivated">?attr/colorControlHighlight</item>
+ <item name="listPreferredItemPaddingStart">?attr/dialogPreferredPadding</item>
+ <item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>
+
+ <!-- Dialog attributes -->
+ <item name="dialogCornerRadius">@dimen/config_dialogCornerRadius</item>
+ <item name="alertDialogTheme">@style/Theme.DeviceDefault.Light.Dialog.Alert</item>
+
+ <!-- Button styles -->
+ <item name="buttonCornerRadius">@dimen/config_buttonCornerRadius</item>
+ <item name="buttonBarButtonStyle">@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+ <item name="borderlessButtonStyle">@style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+ <item name="buttonStyle">@style/Widget.DeviceDefault.Button</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimary">@color/primary_device_default_light</item>
+ <item name="colorPrimaryDark">@color/primary_device_default_dark</item>
+ <item name="colorAccent">@color/accent_device_default_light</item>
+ <item name="colorError">@color/error_color_device_default_light</item>
+ <item name="colorBackgroundFloating">@color/background_floating_device_default_light</item>
+ <item name="colorButtonNormal">@color/car_highlight</item>
+ <item name="colorControlHighlight">@color/car_card_ripple_background</item>
+ <item name="colorControlNormal">@color/car_body2</item>
+ <item name="colorForeground">@color/car_card_light</item>
+ <item name="editTextColor">@color/car_body1</item>
+ <item name="textColorHint">@color/car_body2</item>
+ <item name="textColorPrimary">@color/car_body1</item>
+ <item name="textColorSecondary">@color/car_body2</item>
+
+ <!-- Progress bar attributes -->
+ <item name="colorProgressBackgroundNormal">@color/config_progress_background_tint</item>
+ <item name="progressBarCornerRadius">@dimen/config_progressBarCornerRadius</item>
+
+ <!-- Toolbar attributes -->
+ <item name="toolbarStyle">@style/Widget.DeviceDefault.Toolbar</item>
+
+ <item name="toastFrameBackground">@drawable/toast_frame</item>
+ <item name="textAppearanceListItem">@style/TextAppearance.DeviceDefault.Large</item>
+ <item name="textAppearanceListItemSmall">@style/TextAppearance.DeviceDefault.Large</item>
+ <item name="textAppearanceListItemSecondary">@style/TextAppearance.DeviceDefault.Small</item>
+ </style>
</resources>
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());
+ }
+ }
+}