/* * Copyright (C) 2015 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; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING; import static com.android.car.CarServiceUtils.getHandlerThread; import static com.android.car.CarServiceUtils.isEventOfType; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.car.ICarPerUserService; import android.car.builtin.util.Slogf; import android.car.user.CarUserManager.UserLifecycleListener; import android.car.user.UserLifecycleEventFilter; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Handler; import android.os.IBinder; import android.os.UserHandle; import android.util.Log; import android.util.proto.ProtoOutputStream; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; import com.android.car.user.CarUserService; import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; import java.util.List; /** * A Helper class that helps with the following: * 1. Provide methods to Bind/Unbind to the {@link CarPerUserService} as the current User * 2. Set up a listener to UserSwitch Broadcasts and call clients that have registered callbacks. * */ public class CarPerUserServiceHelper implements CarServiceBase { private static final String TAG = CarLog.tagFor(CarPerUserServiceHelper.class); private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); private final Context mContext; private final CarUserService mUserService; private final Handler mHandler; private ICarPerUserService mCarPerUserService; // listener to call on a ServiceConnection to CarPerUserService private List mServiceCallbacks; private final Object mServiceBindLock = new Object(); @GuardedBy("mServiceBindLock") private boolean mBound; public CarPerUserServiceHelper(Context context, CarUserService userService) { mContext = context; mServiceCallbacks = new ArrayList<>(); mUserService = userService; mHandler = new Handler(getHandlerThread( CarPerUserServiceHelper.class.getSimpleName()).getLooper()); UserLifecycleEventFilter userSwitchingEventFilter = new UserLifecycleEventFilter.Builder() .addEventType(USER_LIFECYCLE_EVENT_TYPE_SWITCHING).build(); mUserService.addUserLifecycleListener(userSwitchingEventFilter, mUserLifecycleListener); } @Override public void init() { synchronized (mServiceBindLock) { bindToCarPerUserService(); } } @Override public void release() { synchronized (mServiceBindLock) { unbindFromCarPerUserService(); mUserService.removeUserLifecycleListener(mUserLifecycleListener); } } private final UserLifecycleListener mUserLifecycleListener = event -> { if (!isEventOfType(TAG, event, USER_LIFECYCLE_EVENT_TYPE_SWITCHING)) { return; } if (DBG) { Slogf.d(TAG, "onEvent(" + event + ")"); } List callbacks; int userId = event.getUserId(); if (DBG) { Slogf.d(TAG, "User Switch Happened. New User" + userId); } // Before unbinding, notify the callbacks about unbinding from the service // so the callbacks can clean up their state through the binder before the service is // killed. synchronized (mServiceBindLock) { // copy the callbacks callbacks = new ArrayList<>(mServiceCallbacks); } // call them for (ServiceCallback callback : callbacks) { callback.onPreUnbind(); } // unbind from the service running as the previous user. unbindFromCarPerUserService(); // bind to the service running as the new user bindToCarPerUserService(); }; /** * ServiceConnection to detect connecting/disconnecting to {@link CarPerUserService} */ private final ServiceConnection mUserServiceConnection = new ServiceConnection() { // Handle ServiceConnection on a separate thread because the tasks performed on service // connected/disconnected take long time to complete and block the executing thread. // Executing these tasks on the main thread will result in CarService ANR. // On connecting to the service, get the binder object to the CarBluetoothService @Override public void onServiceConnected(ComponentName componentName, IBinder service) { mHandler.post(() -> { List callbacks; if (DBG) { Slogf.d(TAG, "Connected to User Service"); } mCarPerUserService = ICarPerUserService.Stub.asInterface(service); if (mCarPerUserService != null) { synchronized (mServiceBindLock) { // copy the callbacks callbacks = new ArrayList<>(mServiceCallbacks); } // call them for (ServiceCallback callback : callbacks) { callback.onServiceConnected(mCarPerUserService); } } }); } @Override public void onServiceDisconnected(ComponentName componentName) { mHandler.post(() -> { List callbacks; if (DBG) { Slogf.d(TAG, "Disconnected from User Service"); } synchronized (mServiceBindLock) { // copy the callbacks callbacks = new ArrayList<>(mServiceCallbacks); } // call them for (ServiceCallback callback : callbacks) { callback.onServiceDisconnected(); } }); } }; /** * Bind to the CarPerUserService {@link CarPerUserService} which is created to run as the * Current User. */ private void bindToCarPerUserService() { if (DBG) { Slogf.d(TAG, "Binding to User service"); } // This crosses both process and package boundary. Intent startIntent = BuiltinPackageDependency.addClassNameToIntent(mContext, new Intent(), BuiltinPackageDependency.CAR_USER_PER_SERVICE_CLASS); synchronized (mServiceBindLock) { mBound = true; boolean bindSuccess = mContext.bindServiceAsUser(startIntent, mUserServiceConnection, mContext.BIND_AUTO_CREATE, UserHandle.CURRENT); // If valid connection not obtained, unbind if (!bindSuccess) { Slogf.e(TAG, "bindToCarPerUserService() failed to get valid connection"); unbindFromCarPerUserService(); } } } /** * Unbind from the {@link CarPerUserService} running as the Current user. */ private void unbindFromCarPerUserService() { synchronized (mServiceBindLock) { // mBound flag makes sure we are unbinding only when the service is bound. if (mBound) { if (DBG) { Slogf.d(TAG, "Unbinding from User Service"); } mContext.unbindService(mUserServiceConnection); mBound = false; } } } /** * Register a listener that gets called on Connection state changes to the * {@link CarPerUserService} * @param listener - Callback to invoke on user switch event. */ public void registerServiceCallback(ServiceCallback listener) { if (listener != null) { if (DBG) { Slogf.d(TAG, "Registering CarPerUserService Listener"); } synchronized (mServiceBindLock) { mServiceCallbacks.add(listener); } } } /** * Unregister the Service Listener * @param listener - Callback method to unregister */ public void unregisterServiceCallback(ServiceCallback listener) { if (DBG) { Slogf.d(TAG, "Unregistering CarPerUserService Listener"); } if (listener != null) { synchronized (mServiceBindLock) { mServiceCallbacks.remove(listener); } } } /** * Listener to the CarPerUserService connection status that clients need to implement. */ public interface ServiceCallback { /** * Invoked when a service connects. * * @param carPerUserService the instance of ICarPerUserService. */ void onServiceConnected(ICarPerUserService carPerUserService); /** * Invoked before an unbind call is going to be made. */ void onPreUnbind(); /** * Invoked when a service is crashed or disconnected. */ void onServiceDisconnected(); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public final void dump(IndentingPrintWriter pw) { pw.println("CarPerUserServiceHelper"); pw.increaseIndent(); synchronized (mServiceBindLock) { pw.printf("bound: %b\n", mBound); if (mServiceCallbacks == null) { pw.println("no callbacks"); } else { int size = mServiceCallbacks.size(); pw.printf("%d callback%s\n", size, (size > 1 ? "s" : "")); } } pw.decreaseIndent(); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) {} }