blob: 189370e8301be06d5787b4452bfe9638b418a1eb [file] [log] [blame]
/*
* 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.content.Context.BIND_AUTO_CREATE;
import android.car.userlib.CarUserManagerHelper;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Resources;
import android.os.Debug;
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.user.CarUserService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
/**
* Class that responsible for controlling vendor services that was opted in to be bound/started
* by the Car Service.
*
* <p>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.
*/
class VendorServiceController implements CarUserService.UserCallback {
private static final boolean DBG = true;
private static final int MSG_SWITCH_USER = 1;
private static final int MSG_USER_LOCK_CHANGED = 2;
private final List<VendorServiceInfo> mVendorServiceInfos = new ArrayList<>();
private final HashMap<ConnectionKey, VendorServiceConnection> mConnections =
new HashMap<>();
private final Context mContext;
private final UserManager mUserManager;
private final Handler mHandler;
private final CarUserManagerHelper mUserManagerHelper;
private CarUserService mCarUserService;
VendorServiceController(Context context, Looper looper,
CarUserManagerHelper userManagerHelper) {
mContext = context;
mUserManager = context.getSystemService(UserManager.class);
mUserManagerHelper = userManagerHelper;
mHandler = new Handler(looper) {
@Override
public void handleMessage(Message msg) {
VendorServiceController.this.handleMessage(msg);
}
};
}
private void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SWITCH_USER: {
int userId = msg.arg1;
doSwitchUser(userId);
break;
}
case MSG_USER_LOCK_CHANGED: {
int userId = msg.arg1;
boolean locked = msg.arg2 == 1;
doUserLockChanged(userId, locked);
break;
}
default:
Log.e(CarLog.TAG_PACKAGE, "Unexpected message " + msg);
}
}
void init() {
if (!loadXmlConfiguration()) {
return; // Nothing to do
}
mCarUserService = CarLocalServices.getService(CarUserService.class);
mCarUserService.addUserCallback(this);
startOrBindServicesIfNeeded();
}
void release() {
if (mCarUserService != null) {
mCarUserService.removeUserCallback(this);
}
for (ConnectionKey key : mConnections.keySet()) {
stopOrUnbindService(key.mVendorServiceInfo, key.mUserHandle);
}
mVendorServiceInfos.clear();
mConnections.clear();
}
private void doSwitchUser(int userId) {
// Stop all services which which do not run under foreground or system user.
final int fgUser = mUserManagerHelper.getCurrentForegroundUserId();
if (fgUser != userId) {
Log.w(CarLog.TAG_PACKAGE, "Received userSwitch event for user " + userId
+ " while current foreground user is " + fgUser + "."
+ " Ignore the switch user event.");
return;
}
for (VendorServiceConnection connection : mConnections.values()) {
final int connectedUserId = connection.mUser.getIdentifier();
if (connectedUserId != UserHandle.USER_SYSTEM && connectedUserId != userId) {
connection.stopOrUnbindService();
}
}
if (userId != UserHandle.USER_SYSTEM) {
startOrBindServicesForUser(UserHandle.of(userId));
} else {
Log.e(CarLog.TAG_PACKAGE, "Unexpected to receive switch user event for system user");
}
}
private void doUserLockChanged(int userId, boolean unlocked) {
final int currentUserId = mUserManagerHelper.getCurrentForegroundUserId();
if (DBG) {
Log.i(CarLog.TAG_PACKAGE, "onUserLockedChanged, user: " + userId
+ ", unlocked: " + unlocked + ", currentUser: " + currentUserId);
}
if (unlocked && (userId == currentUserId || userId == UserHandle.USER_SYSTEM)) {
startOrBindServicesForUser(UserHandle.of(userId));
} else if (!unlocked && userId != UserHandle.USER_SYSTEM) {
for (ConnectionKey key : mConnections.keySet()) {
if (key.mUserHandle.getIdentifier() == userId) {
stopOrUnbindService(key.mVendorServiceInfo, key.mUserHandle);
}
}
}
}
private void startOrBindServicesForUser(UserHandle user) {
boolean unlocked = mUserManager.isUserUnlockingOrUnlocked(user);
boolean systemUser = UserHandle.SYSTEM.equals(user);
for (VendorServiceInfo service: mVendorServiceInfos) {
boolean userScopeChecked = (!systemUser && service.isForegroundUserService())
|| (systemUser && service.isSystemUserService());
boolean triggerChecked = service.shouldStartAsap()
|| (unlocked && service.shouldStartOnUnlock());
if (userScopeChecked && triggerChecked) {
startOrBindService(service, user);
}
}
}
private void startOrBindServicesIfNeeded() {
int userId = mUserManagerHelper.getCurrentForegroundUserId();
startOrBindServicesForUser(UserHandle.SYSTEM);
if (userId > 0) {
startOrBindServicesForUser(UserHandle.of(userId));
}
}
@Override
public void onUserLockChanged(int userId, boolean unlocked) {
Message msg = mHandler.obtainMessage(MSG_USER_LOCK_CHANGED, userId, unlocked ? 1 : 0);
mHandler.executeOrSendMessage(msg);
}
@Override
public void onSwitchUser(int userId) {
mHandler.removeMessages(MSG_SWITCH_USER);
Message msg = mHandler.obtainMessage(MSG_SWITCH_USER, userId, 0);
mHandler.executeOrSendMessage(msg);
}
private void startOrBindService(VendorServiceInfo service, UserHandle user) {
ConnectionKey key = ConnectionKey.of(service, user);
VendorServiceConnection connection = getOrCreateConnection(key);
if (!connection.startOrBindService()) {
Log.e(CarLog.TAG_PACKAGE, "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();
}
}
private VendorServiceConnection getOrCreateConnection(ConnectionKey key) {
VendorServiceConnection connection = mConnections.get(key);
if (connection == null) {
connection = new VendorServiceConnection(mContext, mHandler, mUserManagerHelper,
key.mVendorServiceInfo, key.mUserHandle);
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) {
Log.i(CarLog.TAG_PACKAGE, "Registered vendor service: " + service);
}
}
Log.i(CarLog.TAG_PACKAGE, "Found " + mVendorServiceInfos.size()
+ " services to be started/bound");
return !mVendorServiceInfos.isEmpty();
}
/**
* Represents connection to the vendor service.
*/
private static class VendorServiceConnection implements ServiceConnection {
private static final int REBIND_DELAY_MS = 1000;
private static final int MAX_RECENT_FAILURES = 5;
private static final int 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 Context mContext;
private final UserHandle mUser;
private final Handler mHandler;
private final Handler mFailureHandler;
private final CarUserManagerHelper mUserManagerHelper;
VendorServiceConnection(Context context, Handler handler,
CarUserManagerHelper userManagerHelper, VendorServiceInfo vendorServiceInfo,
UserHandle user) {
mContext = context;
mHandler = handler;
mUserManagerHelper = userManagerHelper;
mVendorServiceInfo = vendorServiceInfo;
mUser = user;
mFailureHandler = new Handler(handler.getLooper()) {
@Override
public void handleMessage(Message msg) {
handleFailureMessage(msg);
}
};
}
boolean startOrBindService() {
if (mStarted || mBound) {
return true; // Already started or bound
}
if (DBG) {
Log.d(CarLog.TAG_PACKAGE, "startOrBindService " + mVendorServiceInfo.toShortString()
+ ", as user: " + mUser + ", bind: " + mVendorServiceInfo.shouldBeBound()
+ ", stack: " + Debug.getCallers(5));
}
mStopRequested = false;
Intent intent = mVendorServiceInfo.getIntent();
if (mVendorServiceInfo.shouldBeBound()) {
return mContext.bindServiceAsUser(intent, this, BIND_AUTO_CREATE, mHandler, mUser);
} else if (mVendorServiceInfo.shouldBeStartedInForeground()) {
mStarted = mContext.startForegroundServiceAsUser(intent, mUser) != null;
return mStarted;
} else {
mStarted = mContext.startServiceAsUser(intent, mUser) != null;
return mStarted;
}
}
void stopOrUnbindService() {
mStopRequested = true;
if (mStarted) {
mContext.stopServiceAsUser(mVendorServiceInfo.getIntent(), mUser);
mStarted = false;
} else if (mBound) {
mContext.unbindService(this);
mBound = false;
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBound = true;
if (DBG) {
Log.d(CarLog.TAG_PACKAGE, "onServiceConnected, name: " + name);
}
if (mStopRequested) {
stopOrUnbindService();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
if (DBG) {
Log.d(CarLog.TAG_PACKAGE, "onServiceDisconnected, name: " + name);
}
tryToRebind();
}
@Override
public void onBindingDied(ComponentName name) {
mBound = false;
tryToRebind();
}
private void tryToRebind() {
if (mStopRequested) {
return;
}
if (UserHandle.of(mUserManagerHelper.getCurrentForegroundUserId()).equals(mUser)
|| UserHandle.SYSTEM.equals(mUser)) {
mFailureHandler.sendMessageDelayed(
mFailureHandler.obtainMessage(MSG_REBIND), REBIND_DELAY_MS);
scheduleResetFailureCounter();
} else {
Log.w(CarLog.TAG_PACKAGE, "No need to rebind anymore as the user " + mUser
+ " is no longer in foreground.");
}
}
private void scheduleResetFailureCounter() {
mFailureHandler.removeMessages(MSG_FAILURE_COUNTER_RESET);
mFailureHandler.sendMessageDelayed(
mFailureHandler.obtainMessage(MSG_FAILURE_COUNTER_RESET),
FAILURE_COUNTER_RESET_TIMEOUT);
}
private void handleFailureMessage(Message msg) {
switch (msg.what) {
case MSG_REBIND: {
if (mRecentFailures < MAX_RECENT_FAILURES && !mBound) {
Log.i(CarLog.TAG_PACKAGE, "Attempting to rebind to the service "
+ mVendorServiceInfo.toShortString());
++mRecentFailures;
startOrBindService();
} else {
Log.w(CarLog.TAG_PACKAGE, "Exceeded maximum number of attempts to rebind"
+ "to the service " + mVendorServiceInfo.toShortString());
}
break;
}
case MSG_FAILURE_COUNTER_RESET:
mRecentFailures = 0;
break;
default:
Log.e(CarLog.TAG_PACKAGE,
"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);
}
}
}