/*
* 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.pm;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_INVISIBLE;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_POST_UNLOCKED;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_VISIBLE;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.os.Process.INVALID_UID;
import static com.android.car.CarLog.TAG_AM;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.car.builtin.util.Slogf;
import android.car.hardware.power.CarPowerManager;
import android.car.hardware.power.ICarPowerStateListener;
import android.car.user.CarUserManager.UserLifecycleEvent;
import android.car.user.CarUserManager.UserLifecycleListener;
import android.car.user.UserLifecycleEventFilter;
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.res.Resources;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.R;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.car.power.CarPowerManagementService;
import com.android.car.user.ActivityManagerCurrentUserFetcher;
import com.android.car.user.CarUserService;
import com.android.car.user.CurrentUserFetcher;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/**
* Class that responsible for controlling vendor services that was opted in to be bound/started
* by the Car Service.
*
*
Thread-safety note: all code runs in the {@code Handler} provided in the constructor, whenever
* possible pass {@link #mHandler} when subscribe for callbacks otherwise redirect code to the
* handler.
*/
final class VendorServiceController implements UserLifecycleListener {
@VisibleForTesting
static final String TAG = CarLog.tagFor(VendorServiceController.class);
private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);
private static final String PACKAGE_DATA_SCHEME = "package";
private final List mVendorServiceInfos = new ArrayList<>();
private final Map mConnections =
new ConcurrentHashMap<>();
private final Context mContext;
private final UserManager mUserManager;
private final Handler mHandler;
private CarUserService mCarUserService;
private CarPowerManagementService mPowerManagementService;
private CurrentUserFetcher mCurrentUserFetcher;
private final BroadcastReceiver mPackageChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DBG) {
Slogf.d(TAG_AM, "Package change received with action = %s", action);
}
Uri packageData = intent.getData();
if (packageData == null) {
Slogf.wtf(TAG_AM, "null packageData");
return;
}
String packageName = packageData.getSchemeSpecificPart();
if (packageName == null) {
Slogf.w(TAG_AM, "null packageName");
return;
}
int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
switch (action) {
case Intent.ACTION_PACKAGE_CHANGED:
// Fall through
case Intent.ACTION_PACKAGE_REPLACED:
// Fall through
case Intent.ACTION_PACKAGE_ADDED:
startOrBindServiceForPackage(packageName, userId);
break;
case Intent.ACTION_PACKAGE_REMOVED:
stopOrUnbindService(packageName, userId);
break;
default:
Slogf.w(TAG_AM, "This package change event (%s) can't be handled.",
action);
}
}
};
private final ICarPowerStateListener mCarPowerStateListener =
new ICarPowerStateListener.Stub() {
@Override
public void onStateChanged(int state, long expirationTimeMs) {
if (DBG) {
Slogf.d(TAG, "Power state change received. State = %d", state);
}
if (state == CarPowerManager.STATE_HIBERNATION_EXIT
|| state == CarPowerManager.STATE_SUSPEND_EXIT) {
onPowerResumed();
}
}
};
VendorServiceController(Context context, Looper looper) {
this(context, looper, new ActivityManagerCurrentUserFetcher());
}
@VisibleForTesting
VendorServiceController(Context context, Looper looper,
CurrentUserFetcher currentUserFetcher) {
mContext = context;
mUserManager = context.getSystemService(UserManager.class);
mHandler = new Handler(looper);
mCurrentUserFetcher = currentUserFetcher;
}
void init() {
if (!loadXmlConfiguration()) {
return; // Nothing to do
}
mPowerManagementService = CarLocalServices.getService(CarPowerManagementService.class);
mCarUserService = CarLocalServices.getService(CarUserService.class);
UserLifecycleEventFilter userLifecycleEventFilter =
new UserLifecycleEventFilter.Builder()
.addEventType(USER_LIFECYCLE_EVENT_TYPE_SWITCHING)
.addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
.addEventType(USER_LIFECYCLE_EVENT_TYPE_VISIBLE)
.addEventType(USER_LIFECYCLE_EVENT_TYPE_INVISIBLE)
.addEventType(USER_LIFECYCLE_EVENT_TYPE_POST_UNLOCKED).build();
mCarUserService.addUserLifecycleListener(userLifecycleEventFilter, this);
startOrBindServicesIfNeeded();
mPowerManagementService.registerListener(mCarPowerStateListener);
registerPackageChangeReceiver();
}
void release() {
if (mVendorServiceInfos.isEmpty()) {
Slogf.d(TAG_AM, "Releasing VendorServiceController without deep cleaning as no vendor "
+ "service info present. ");
return;
}
if (mCarUserService != null) {
mCarUserService.removeUserLifecycleListener(this);
}
unregisterPackageChangeReceiver();
mPowerManagementService.unregisterListener(mCarPowerStateListener);
for (ConnectionKey key : mConnections.keySet()) {
stopOrUnbindService(key.mVendorServiceInfo, key.mUserHandle);
}
mVendorServiceInfos.clear();
mConnections.clear();
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
public void dump(IndentingPrintWriter writer) {
writer.println("VendorServiceController:");
writer.increaseIndent();
writer.printf("DBG=%b\n", DBG);
writer.println("VendorServiceInfo:");
writer.increaseIndent();
for (VendorServiceInfo info : mVendorServiceInfos) {
writer.println(info.toString());
}
writer.decreaseIndent(); // end of VendorServiceInfo:
writer.println("Connections:");
writer.increaseIndent();
for (VendorServiceConnection connection : mConnections.values()) {
connection.dump(writer);
}
writer.decreaseIndent(); // end of Connections:
writer.decreaseIndent(); // end of VendorServiceController:
}
@Override
public void onEvent(UserLifecycleEvent event) {
if (DBG) {
Slogf.d(TAG, "onEvent(" + event + ")");
}
int userId = event.getUserId();
switch (event.getEventType()) {
case USER_LIFECYCLE_EVENT_TYPE_VISIBLE:
mHandler.post(() -> handleOnUserVisible(userId));
break;
case USER_LIFECYCLE_EVENT_TYPE_INVISIBLE:
mHandler.post(() -> handleOnUserInvisible(userId));
break;
case USER_LIFECYCLE_EVENT_TYPE_SWITCHING:
mHandler.post(() -> handleOnUserSwitching(userId));
break;
case USER_LIFECYCLE_EVENT_TYPE_UNLOCKED:
mHandler.post(() -> handleOnUserUnlocked(userId, /* forPostUnlock= */ false));
break;
case USER_LIFECYCLE_EVENT_TYPE_POST_UNLOCKED:
mHandler.post(() -> handleOnUserUnlocked(userId, /* forPostUnlock= */ true));
break;
default:
// Shouldn't happen as listener was registered with filter
Slogf.wtf(TAG, "Invalid event: %s", event);
}
}
/** Handles power resume events, starting services with `trigger=resume`. */
private void onPowerResumed() {
if (DBG) {
Slogf.d(TAG, "onPowerResumed()");
}
int size = mVendorServiceInfos.size();
for (int i = 0; i < size; i++) {
VendorServiceInfo serviceInfo = mVendorServiceInfos.get(i);
// RESUME events handle the system user only. Current or visible users are handled by
// user lifecycle events (unlock, visible, etc).
boolean isForSystemOrAllUsers = serviceInfo.isSystemUserService();
boolean isResumeTrigger = serviceInfo.shouldStartOnResume();
if (isForSystemOrAllUsers && isResumeTrigger) {
startOrBindService(serviceInfo, UserHandle.SYSTEM);
}
}
}
private void registerPackageChangeReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme(PACKAGE_DATA_SCHEME);
mContext.registerReceiverForAllUsers(mPackageChangeReceiver, filter,
/* broadcastPermission= */ null, /* scheduler= */ null,
Context.RECEIVER_NOT_EXPORTED);
}
private void unregisterPackageChangeReceiver() {
mContext.unregisterReceiver(mPackageChangeReceiver);
}
private void startOrBindServiceForPackage(String packageName, @UserIdInt int userId) {
if (DBG) {
Slogf.d(TAG, "startOrBindServiceForPackage() for package=%s, userId=%d",
packageName, userId);
}
int currentUserId = mCurrentUserFetcher.getCurrentUser();
int size = mVendorServiceInfos.size();
for (int i = 0; i < size; i++) {
VendorServiceInfo serviceInfo = mVendorServiceInfos.get(i);
// Start or bind the service when the package name matches and the user is in scope.
if (packageName.equals(serviceInfo.getIntent().getComponent().getPackageName())
&& isUserInScope(userId, serviceInfo, mCarUserService, currentUserId)) {
startOrBindService(serviceInfo, UserHandle.of(userId));
}
}
}
/** Checks if the given {@code serviceInfo} satisfies the user scope. */
private static boolean isUserInScope(@UserIdInt int userId, VendorServiceInfo serviceInfo,
CarUserService carUserService, @UserIdInt int currentUserId) {
boolean isSystemUser = userId == UserHandle.SYSTEM.getIdentifier();
boolean isCurrentUser = userId == currentUserId;
return (isSystemUser && serviceInfo.isSystemUserService())
|| (!isSystemUser && isCurrentUser && serviceInfo.isForegroundUserService())
|| ((serviceInfo.isVisibleUserService()
|| (!isCurrentUser && serviceInfo.isBackgroundVisibleUserService()))
&& carUserService.isUserVisible(userId));
}
private void handleOnUserSwitching(@UserIdInt int userId) {
// The user switch notification is obsolete if userId is different from the current
// foreground user. Ignore it.
int currentUserId = mCurrentUserFetcher.getCurrentUser();
if (currentUserId != userId) {
Slogf.w(TAG, "Received userSwitch event for user " + userId
+ " while current foreground user is " + currentUserId + "."
+ " Ignore the switch user event.");
return;
}
// Clean up the services which do not satisfy their configured user scope.
for (VendorServiceConnection connection : mConnections.values()) {
int connectedUserId = connection.mUser.getIdentifier();
if (!isUserInScope(connectedUserId, connection.mVendorServiceInfo, mCarUserService,
currentUserId)) {
connection.stopOrUnbindService();
}
}
if (userId != UserHandle.SYSTEM.getIdentifier()) {
startOrBindServicesForUser(UserHandle.of(userId), /* forPostUnlock= */ null);
} else {
Slogf.wtf(TAG, "Unexpected to receive switch user event for system user");
}
}
private void handleOnUserInvisible(@UserIdInt int userId) {
if (DBG) {
Slogf.d(TAG, "handleOnUserInvisible(): user=%d", userId);
}
for (VendorServiceConnection connection : mConnections.values()) {
VendorServiceInfo serviceInfo = connection.mVendorServiceInfo;
if (connection.isUser(userId)
&& (serviceInfo.isVisibleUserService()
|| serviceInfo.isBackgroundVisibleUserService())
&& !serviceInfo.isAllUserService()) {
connection.stopOrUnbindService();
}
}
}
private void handleOnUserVisible(@UserIdInt int userId) {
if (DBG) {
Slogf.d(TAG, "handleOnUserVisible(): user=%d", userId);
}
startOrBindServicesForUser(UserHandle.of(userId), /* forPostUnlock= */ null);
}
private void handleOnUserUnlocked(@UserIdInt int userId, boolean forPostUnlock) {
if (DBG) {
Slogf.d(TAG, "handleOnUserUnlocked(): user=%d", userId);
}
startOrBindServicesForUser(UserHandle.of(userId), forPostUnlock);
}
private void startOrBindServicesForUser(UserHandle user, @Nullable Boolean forPostUnlock) {
int userId = user.getIdentifier();
if (!mUserManager.isUserRunning(user)) {
Slogf.w(TAG, "User %d is not running, skip startOrBindServicesForUser", userId);
return;
}
boolean unlocked = mUserManager.isUserUnlockingOrUnlocked(user);
int currentUserId = mCurrentUserFetcher.getCurrentUser();
for (VendorServiceInfo service: mVendorServiceInfos) {
if (forPostUnlock != null
&& service.shouldStartOnPostUnlock() != forPostUnlock.booleanValue()) {
continue;
}
boolean userScopeChecked = isUserInScope(userId, service, mCarUserService,
currentUserId);
boolean triggerChecked = service.shouldStartAsap() || unlocked;
if (userScopeChecked && triggerChecked) {
startOrBindService(service, user);
}
}
}
@SuppressLint("NewApi")
private void startOrBindServicesIfNeeded() {
// Start/bind service for system user.
startOrBindServicesForUser(UserHandle.SYSTEM, /* forPostUnlock= */ null);
// Start/bind service for all visible users.
Set visibleUsers = mUserManager.getVisibleUsers();
for (Iterator iterator = visibleUsers.iterator(); iterator.hasNext();) {
UserHandle userHandle = iterator.next();
startOrBindServicesForUser(userHandle, /* forPostUnlock= */ null);
}
}
private void startOrBindService(VendorServiceInfo service, UserHandle user) {
ConnectionKey key = ConnectionKey.of(service, user);
VendorServiceConnection connection = getOrCreateConnection(key);
if (!connection.startOrBindService()) {
Slogf.e(TAG, "Failed to start or bind service " + service);
mConnections.remove(key);
}
}
private void stopOrUnbindService(VendorServiceInfo service, UserHandle user) {
ConnectionKey key = ConnectionKey.of(service, user);
VendorServiceConnection connection = mConnections.get(key);
if (connection != null) {
connection.stopOrUnbindService();
}
}
/**
* Unbinds the VendorServiceController from all the services with the given {@code packageName}
* and running as {@code userId}.
*/
private void stopOrUnbindService(String packageName, @UserIdInt int userId) {
for (VendorServiceConnection connection : mConnections.values()) {
if (connection.isUser(userId)
&& packageName.equals(connection.mVendorServiceInfo.getIntent().getComponent()
.getPackageName())) {
Slogf.d(TAG, "Stopping the connection to service %s",
connection.mVendorServiceInfo);
connection.stopOrUnbindService();
}
}
}
private VendorServiceConnection getOrCreateConnection(ConnectionKey key) {
VendorServiceConnection connection = mConnections.get(key);
if (connection == null) {
connection = new VendorServiceConnection(mContext, mHandler, key.mVendorServiceInfo,
key.mUserHandle, mCurrentUserFetcher);
mConnections.put(key, connection);
}
return connection;
}
/** Loads data from XML resources and returns true if any services needs to be started/bound. */
private boolean loadXmlConfiguration() {
final Resources res = mContext.getResources();
for (String rawServiceInfo: res.getStringArray(R.array.config_earlyStartupServices)) {
if (TextUtils.isEmpty(rawServiceInfo)) {
continue;
}
VendorServiceInfo service = VendorServiceInfo.parse(rawServiceInfo);
mVendorServiceInfos.add(service);
if (DBG) {
Slogf.i(TAG, "Registered vendor service: " + service);
}
}
Slogf.i(TAG, "Found " + mVendorServiceInfos.size()
+ " services to be started/bound");
return !mVendorServiceInfos.isEmpty();
}
/**
* Represents connection to the vendor service.
*/
@VisibleForTesting
public static final class VendorServiceConnection implements ServiceConnection, Executor {
private static final int INITIAL_REBIND_DELAY_MS = 4000; // 4 sec.
private static final int DEFAULT_FAILURE_COUNTER_RESET_TIMEOUT = 5 * 60 * 1000; // 5 min.
private static final int MSG_REBIND = 0;
private static final int MSG_FAILURE_COUNTER_RESET = 1;
private int mRecentFailures = 0;
private boolean mBound = false;
private boolean mStarted = false;
private boolean mStopRequested = false;
private final VendorServiceInfo mVendorServiceInfo;
private final UserHandle mUser;
private final CarUserService mCarUserService;
private final Context mUserContext;
private final Handler mHandler;
private final Handler mFailureHandler;
private final CurrentUserFetcher mCurrentUserFetcher;
VendorServiceConnection(Context context, Handler handler,
VendorServiceInfo vendorServiceInfo, UserHandle user,
CurrentUserFetcher currentUserFetcher) {
mHandler = handler;
mVendorServiceInfo = vendorServiceInfo;
mUser = user;
mUserContext = context.createContextAsUser(mUser, /* flags= */ 0);
mCarUserService = CarLocalServices.getService(CarUserService.class);
mCurrentUserFetcher = currentUserFetcher;
mFailureHandler = new Handler(handler.getLooper()) {
@Override
public void handleMessage(Message msg) {
handleFailureMessage(msg);
}
};
}
@Override
public String toString() {
return "VendorServiceConnection[user=" + mUser
+ ", service=" + mVendorServiceInfo + "]";
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
public void dump(PrintWriter writer) {
writer.printf("%s, mRecentFailures=%d, mBound=%b, mStarted=%b, mStopRequested=%b\n",
toString(), mRecentFailures, mBound, mStarted, mStopRequested);
}
private boolean isUser(@UserIdInt int userId) {
return mUser.getIdentifier() == userId;
}
boolean startOrBindService() {
if (mStarted || mBound) {
return true; // Already started or bound
}
if (DBG) {
Slogf.d(TAG, "startOrBindService "
+ mVendorServiceInfo.toShortString() + ", as user: " + mUser + ", bind: "
+ mVendorServiceInfo.shouldBeBound());
}
mStopRequested = false;
Intent intent = mVendorServiceInfo.getIntent();
if (mVendorServiceInfo.shouldBeBound()) {
boolean canBind = mUserContext.bindService(intent, BIND_AUTO_CREATE,
/* executor= */ this, /* conn= */ this);
if (!canBind) {
// Still need to unbind when an attempt to bind fails.
try {
unbindService();
} catch (Exception e) {
// When binding already failed, log and ignore an exception from unbind.
Slogf.w(TAG, "After bindService() failed, unbindService() threw "
+ "an exception:", e);
}
}
return canBind;
} else if (mVendorServiceInfo.shouldBeStartedInForeground()) {
try {
mStarted = mUserContext.startForegroundService(intent) != null;
} catch (SecurityException e) {
Slogf.e(TAG, "ERROR: Failed to start fg service : " + e);
mStarted = false;
}
return mStarted;
} else {
try {
mStarted = mUserContext.startService(intent) != null;
} catch (SecurityException e) {
Slogf.e(TAG, "ERROR: Failed to start service : " + e);
mStarted = false;
}
return mStarted;
}
}
void stopOrUnbindService() {
mStopRequested = true;
if (mStarted) {
if (DBG) Slogf.d(TAG, "Stopping %s", this);
mUserContext.stopService(mVendorServiceInfo.getIntent());
mStarted = false;
} else if (mBound) {
unbindService();
mBound = false;
}
}
private void unbindService() {
if (DBG) Slogf.d(TAG, "Unbinding %s", this);
mUserContext.unbindService(this);
}
@Override // From Executor
public void execute(Runnable command) {
mHandler.post(command);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBound = true;
if (DBG) {
Slogf.d(TAG, "onServiceConnected, name: %s", name);
}
if (mStopRequested) {
stopOrUnbindService();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (DBG) {
Slogf.d(TAG, "onServiceDisconnected, name: " + name);
}
// A binding is persistent, and the service will be reconnected by the binder.
// Therefore, there is no need to attempt to rebind or reconnect here.
}
@Override
public void onBindingDied(ComponentName name) {
mBound = false;
if (DBG) {
Slogf.d(TAG, "onBindingDied, name: " + name);
}
// When a binding died, first unbind the connection and then rebind.
unbindService();
tryToRebind();
}
@Override
public void onNullBinding(ComponentName name) {
// Null binding means that the attempted service will never become usable.
if (DBG) {
Slogf.d(TAG, "onNullBinding, name: " + name);
}
// Still need to unbind to release resource associated with the connection.
unbindService();
}
private void tryToRebind() {
if (mStopRequested) {
return;
}
if (mFailureHandler.hasMessages(MSG_REBIND)) {
if (DBG) {
Slogf.d(TAG, "Rebind already scheduled for "
+ mVendorServiceInfo.toShortString());
}
return;
}
int currentUserId = mCurrentUserFetcher.getCurrentUser();
if (isUserInScope(mUser.getIdentifier(), mVendorServiceInfo, mCarUserService,
currentUserId)) {
// Double the delay after each failure.
int rebindDelay = INITIAL_REBIND_DELAY_MS * (1 << mRecentFailures);
Slogf.i(TAG, "tryToRebind(): after " + mRecentFailures + " recent failures,"
+ " trying to rebind service " + mVendorServiceInfo.toShortString()
+ " for user " + mUser.getIdentifier() + " in " + rebindDelay + "ms");
mFailureHandler.sendMessageDelayed(
mFailureHandler.obtainMessage(MSG_REBIND), rebindDelay);
scheduleResetFailureCounter();
} else {
Slogf.w(TAG, "No need to rebind anymore as the service no longer satisfies "
+ " the user scope.");
}
}
private void scheduleResetFailureCounter() {
mFailureHandler.removeMessages(MSG_FAILURE_COUNTER_RESET);
// Reset the failure counter after the timeout. We take the max, to ensure
// that we are not resetting the counter before exhausting all retries.
int failureCounterResetTimeout =
INITIAL_REBIND_DELAY_MS * (1 << (mVendorServiceInfo.getMaxRetries() + 1));
failureCounterResetTimeout =
failureCounterResetTimeout > DEFAULT_FAILURE_COUNTER_RESET_TIMEOUT
? failureCounterResetTimeout : DEFAULT_FAILURE_COUNTER_RESET_TIMEOUT;
mFailureHandler.sendMessageDelayed(
mFailureHandler.obtainMessage(MSG_FAILURE_COUNTER_RESET),
failureCounterResetTimeout);
}
private void handleFailureMessage(Message msg) {
switch (msg.what) {
case MSG_REBIND: {
if (mBound) {
Slogf.d(TAG, "Service " + mVendorServiceInfo.toShortString()
+ " is already bound. Ignoring MSG_REBIND");
} else if (mRecentFailures < mVendorServiceInfo.getMaxRetries()) {
Slogf.i(TAG, "Attempting to rebind to the service "
+ mVendorServiceInfo.toShortString() + " (" + (mRecentFailures + 1)
+ " out of " + mVendorServiceInfo.getMaxRetries() + " max tries)");
++mRecentFailures;
startOrBindService();
} else {
Slogf.w(TAG, "Exceeded maximum number of attempts ("
+ mVendorServiceInfo.getMaxRetries() + ") to rebind to the service "
+ mVendorServiceInfo.toShortString());
}
break;
}
case MSG_FAILURE_COUNTER_RESET:
mRecentFailures = 0;
break;
default:
Slogf.e(TAG, "Unexpected message received in failure handler: " + msg.what);
}
}
}
/** Defines a key in the HashMap to store connection on per user and vendor service basis */
private static class ConnectionKey {
private final UserHandle mUserHandle;
private final VendorServiceInfo mVendorServiceInfo;
private ConnectionKey(VendorServiceInfo service, UserHandle user) {
mVendorServiceInfo = service;
mUserHandle = user;
}
static ConnectionKey of(VendorServiceInfo service, UserHandle user) {
return new ConnectionKey(service, user);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ConnectionKey)) {
return false;
}
ConnectionKey that = (ConnectionKey) o;
return Objects.equals(mUserHandle, that.mUserHandle)
&& Objects.equals(mVendorServiceInfo, that.mVendorServiceInfo);
}
@Override
public int hashCode() {
return Objects.hash(mUserHandle, mVendorServiceInfo);
}
}
}