| /* |
| * Copyright (C) 2016 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 android.car.CarProjectionManager; |
| import android.car.ICarProjection; |
| import android.car.ICarProjectionCallback; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| |
| import java.io.PrintWriter; |
| |
| /** |
| * Car projection service allows to bound to projected app to boost it prioirity. |
| * It also enables proejcted applications to handle voice action requests. |
| */ |
| class CarProjectionService extends ICarProjection.Stub implements CarServiceBase, |
| BinderInterfaceContainer.BinderEventHandler<ICarProjectionCallback> { |
| private final ListenerHolder mAllListeners; |
| private final CarInputService mCarInputService; |
| private final Context mContext; |
| |
| private final CarInputService.KeyEventListener mVoiceAssistantKeyListener = |
| new CarInputService.KeyEventListener() { |
| @Override |
| public boolean onKeyEvent(KeyEvent event) { |
| handleVoiceAssitantRequest(false); |
| return true; |
| } |
| }; |
| |
| private final CarInputService.KeyEventListener mLongVoiceAssistantKeyListener = |
| new CarInputService.KeyEventListener() { |
| @Override |
| public boolean onKeyEvent(KeyEvent event) { |
| handleVoiceAssitantRequest(true); |
| return true; |
| } |
| }; |
| |
| private final ServiceConnection mConnection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName className, IBinder service) { |
| synchronized (CarProjectionService.this) { |
| mBound = true; |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName className) { |
| // Service has crashed. |
| Log.w(CarLog.TAG_PROJECTION, "Service disconnected: " + className); |
| synchronized (CarProjectionService.this) { |
| mRegisteredService = null; |
| } |
| unbindServiceIfBound(); |
| } |
| }; |
| |
| private boolean mBound; |
| private Intent mRegisteredService; |
| |
| CarProjectionService(Context context, CarInputService carInputService) { |
| mContext = context; |
| mCarInputService = carInputService; |
| mAllListeners = new ListenerHolder(this); |
| } |
| |
| @Override |
| public void registerProjectionRunner(Intent serviceIntent) { |
| // We assume one active projection app running in the system at one time. |
| synchronized (this) { |
| if (serviceIntent.filterEquals(mRegisteredService)) { |
| return; |
| } |
| if (mRegisteredService != null) { |
| Log.w(CarLog.TAG_PROJECTION, "Registering new service[" + serviceIntent |
| + "] while old service[" + mRegisteredService + "] is still running"); |
| } |
| unbindServiceIfBound(); |
| } |
| bindToService(serviceIntent); |
| } |
| |
| @Override |
| public void unregisterProjectionRunner(Intent serviceIntent) { |
| synchronized (this) { |
| if (!serviceIntent.filterEquals(mRegisteredService)) { |
| Log.w(CarLog.TAG_PROJECTION, "Request to unbind unregistered service[" |
| + serviceIntent + "]. Registered service[" + mRegisteredService + "]"); |
| return; |
| } |
| mRegisteredService = null; |
| } |
| unbindServiceIfBound(); |
| } |
| |
| private void bindToService(Intent serviceIntent) { |
| synchronized (this) { |
| mRegisteredService = serviceIntent; |
| } |
| UserHandle userHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid()); |
| mContext.bindServiceAsUser(serviceIntent, mConnection, Context.BIND_AUTO_CREATE, |
| userHandle); |
| } |
| |
| private void unbindServiceIfBound() { |
| synchronized (this) { |
| if (!mBound) { |
| return; |
| } |
| mBound = false; |
| } |
| mContext.unbindService(mConnection); |
| } |
| |
| private synchronized void handleVoiceAssitantRequest(boolean isTriggeredByLongPress) { |
| for (BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> listener : |
| mAllListeners.getInterfaces()) { |
| ListenerInfo listenerInfo = (ListenerInfo) listener; |
| if ((listenerInfo.hasFilter(CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH) |
| && isTriggeredByLongPress) |
| || (listenerInfo.hasFilter(CarProjectionManager.PROJECTION_VOICE_SEARCH) |
| && !isTriggeredByLongPress)) { |
| dispatchVoiceAssistantRequest(listenerInfo.binderInterface, isTriggeredByLongPress); |
| } |
| } |
| } |
| |
| @Override |
| public void regsiterProjectionListener(ICarProjectionCallback listener, int filter) { |
| synchronized (this) { |
| ListenerInfo info = (ListenerInfo) mAllListeners.getBinderInterface(listener); |
| if (info == null) { |
| info = new ListenerInfo(mAllListeners, listener, filter); |
| mAllListeners.addBinderInterface(info); |
| } else { |
| info.setFilter(filter); |
| } |
| } |
| updateCarInputServiceListeners(); |
| } |
| |
| @Override |
| public void unregsiterProjectionListener(ICarProjectionCallback listener) { |
| synchronized (this) { |
| mAllListeners.removeBinder(listener); |
| } |
| updateCarInputServiceListeners(); |
| } |
| |
| private void updateCarInputServiceListeners() { |
| boolean listenShortPress = false; |
| boolean listenLongPress = false; |
| synchronized (this) { |
| for (BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> listener : |
| mAllListeners.getInterfaces()) { |
| ListenerInfo listenerInfo = (ListenerInfo) listener; |
| listenShortPress |= listenerInfo.hasFilter( |
| CarProjectionManager.PROJECTION_VOICE_SEARCH); |
| listenLongPress |= listenerInfo.hasFilter( |
| CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH); |
| } |
| } |
| mCarInputService.setVoiceAssistantKeyListener(listenShortPress |
| ? mVoiceAssistantKeyListener : null); |
| mCarInputService.setLongVoiceAssistantKeyListener(listenLongPress |
| ? mLongVoiceAssistantKeyListener : null); |
| } |
| |
| @Override |
| public void init() { |
| // nothing to do |
| } |
| |
| @Override |
| public void release() { |
| synchronized (this) { |
| mAllListeners.clear(); |
| } |
| } |
| |
| @Override |
| public void onBinderDeath( |
| BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> bInterface) { |
| unregsiterProjectionListener(bInterface.binderInterface); |
| } |
| |
| @Override |
| public void dump(PrintWriter writer) { |
| writer.println("**CarProjectionService**"); |
| synchronized (this) { |
| for (BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> listener : |
| mAllListeners.getInterfaces()) { |
| ListenerInfo listenerInfo = (ListenerInfo) listener; |
| writer.println(listenerInfo.toString()); |
| } |
| } |
| } |
| |
| private void dispatchVoiceAssistantRequest(ICarProjectionCallback listener, |
| boolean fromLongPress) { |
| try { |
| listener.onVoiceAssistantRequest(fromLongPress); |
| } catch (RemoteException e) { |
| } |
| } |
| |
| private static class ListenerHolder extends BinderInterfaceContainer<ICarProjectionCallback> { |
| private ListenerHolder(CarProjectionService service) { |
| super(service); |
| } |
| } |
| |
| private static class ListenerInfo extends |
| BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> { |
| private int mFilter; |
| |
| private ListenerInfo(ListenerHolder holder, ICarProjectionCallback binder, int filter) { |
| super(holder, binder); |
| this.mFilter = filter; |
| } |
| |
| private synchronized int getFilter() { |
| return mFilter; |
| } |
| |
| private boolean hasFilter(int filter) { |
| return (getFilter() & filter) != 0; |
| } |
| |
| private synchronized void setFilter(int filter) { |
| mFilter = filter; |
| } |
| |
| @Override |
| public String toString() { |
| synchronized (this) { |
| return "ListenerInfo{filter=" + Integer.toHexString(mFilter) + "}"; |
| } |
| } |
| } |
| } |