blob: e67ea9171a502e1d691bf76c15bc90743788e30e [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.app.ActivityManager;
import android.car.user.CarUserManager;
import android.car.user.CarUserManager.UserLifecycleEvent;
import android.car.user.CarUserManager.UserLifecycleListener;
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 android.util.Slog;
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 UserLifecycleListener {
private static final String TAG = CarLog.tagFor(VendorServiceController.class);
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 CarUserService mCarUserService;
VendorServiceController(Context context, Looper looper) {
mContext = context;
mUserManager = context.getSystemService(UserManager.class);
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:
Slog.e(TAG, "Unexpected message " + msg);
}
}
void init() {
if (!loadXmlConfiguration()) {
return; // Nothing to do
}
mCarUserService = CarLocalServices.getService(CarUserService.class);
mCarUserService.addUserLifecycleListener(this);
startOrBindServicesIfNeeded();
}
void release() {
if (mCarUserService != null) {
mCarUserService.removeUserLifecycleListener(this);
}
for (ConnectionKey key : mConnections.keySet()) {
stopOrUnbindService(key.mVendorServiceInfo, key.mUserHandle);
}
mVendorServiceInfos.clear();
mConnections.clear();
}
@Override
public void onEvent(UserLifecycleEvent event) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Slog.d(TAG, "onEvent(" + event + ")");
}
// TODO(b/152069895): Use USER_LIFECYCLE_EVENT_TYPE_UNLOCKED and not care about the
// deprecated unlock=false scenario.
if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING == event.getEventType()) {
Message msg = mHandler.obtainMessage(
MSG_USER_LOCK_CHANGED,
event.getUserId(),
/* unlocked= */ 1);
mHandler.executeOrSendMessage(msg);
} else if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) {
mHandler.removeMessages(MSG_SWITCH_USER);
Message msg = mHandler.obtainMessage(
MSG_SWITCH_USER,
event.getUserId(),
/* unlocked= */ 0);
mHandler.executeOrSendMessage(msg);
}
}
private void doSwitchUser(int userId) {
// Stop all services which which do not run under foreground or system user.
final int fgUser = ActivityManager.getCurrentUser();
if (fgUser != userId) {
Slog.w(TAG, "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 {
Slog.e(TAG, "Unexpected to receive switch user event for system user");
}
}
private void doUserLockChanged(int userId, boolean unlocked) {
final int currentUserId = ActivityManager.getCurrentUser();
if (DBG) {
Slog.i(TAG, "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 = ActivityManager.getCurrentUser();
startOrBindServicesForUser(UserHandle.SYSTEM);
if (userId > 0) {
startOrBindServicesForUser(UserHandle.of(userId));
}
}
private void startOrBindService(VendorServiceInfo service, UserHandle user) {
ConnectionKey key = ConnectionKey.of(service, user);
VendorServiceConnection connection = getOrCreateConnection(key);
if (!connection.startOrBindService()) {
Slog.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();
}
}
private VendorServiceConnection getOrCreateConnection(ConnectionKey key) {
VendorServiceConnection connection = mConnections.get(key);
if (connection == null) {
connection = new VendorServiceConnection(mContext, mHandler, 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) {
Slog.i(TAG, "Registered vendor service: " + service);
}
}
Slog.i(TAG, "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 = 5000;
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;
VendorServiceConnection(Context context, Handler handler,
VendorServiceInfo vendorServiceInfo, UserHandle user) {
mContext = context;
mHandler = handler;
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) {
Slog.d(TAG, "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) {
Slog.d(TAG, "onServiceConnected, name: " + name);
}
if (mStopRequested) {
stopOrUnbindService();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
if (DBG) {
Slog.d(TAG, "onServiceDisconnected, name: " + name);
}
tryToRebind();
}
@Override
public void onBindingDied(ComponentName name) {
mBound = false;
if (DBG) {
Slog.d(TAG, "onBindingDied, name: " + name);
}
tryToRebind();
}
private void tryToRebind() {
if (mStopRequested) {
return;
}
if (mFailureHandler.hasMessages(MSG_REBIND)) {
if (DBG) {
Slog.d(TAG, "Rebind already scheduled for "
+ mVendorServiceInfo.toShortString());
}
return;
}
if (UserHandle.of(ActivityManager.getCurrentUser()).equals(mUser)
|| UserHandle.SYSTEM.equals(mUser)) {
mFailureHandler.sendMessageDelayed(
mFailureHandler.obtainMessage(MSG_REBIND), REBIND_DELAY_MS);
scheduleResetFailureCounter();
} else {
Slog.w(TAG, "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) {
Slog.i(TAG, "Attempting to rebind to the service "
+ mVendorServiceInfo.toShortString());
++mRecentFailures;
startOrBindService();
} else {
Slog.w(TAG, "Exceeded maximum number of attempts to rebind"
+ "to the service " + mVendorServiceInfo.toShortString());
}
break;
}
case MSG_FAILURE_COUNTER_RESET:
mRecentFailures = 0;
break;
default:
Slog.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);
}
}
}