/* * Copyright 2017 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.server.telecom; import android.annotation.Nullable; import android.content.ComponentName; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.telecom.Log; import android.telecom.Logging.Session; import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class ConnectionServiceFocusManager { private static final String TAG = "ConnectionSvrFocusMgr"; private static final int GET_CURRENT_FOCUS_TIMEOUT_MILLIS = 1000; /** Factory interface used to create the {@link ConnectionServiceFocusManager} instance. */ public interface ConnectionServiceFocusManagerFactory { ConnectionServiceFocusManager create(CallsManagerRequester requester); } /** * Interface used by ConnectionServiceFocusManager to communicate with * {@link ConnectionServiceWrapper}. */ public interface ConnectionServiceFocus { /** * Notifies the {@link android.telecom.ConnectionService} that it has lose the connection * service focus. It should release all call resource i.e camera, audio once it lost the * focus. */ void connectionServiceFocusLost(); /** * Notifies the {@link android.telecom.ConnectionService} that it has gain the connection * service focus. It can request the call resource i.e camera, audio as they expected to be * free at the moment. */ void connectionServiceFocusGained(); /** * Sets the ConnectionServiceFocusListener. * * @see {@link ConnectionServiceFocusListener}. */ void setConnectionServiceFocusListener(ConnectionServiceFocusListener listener); /** * Get the {@link ComponentName} of the ConnectionService for logging purposes. * @return the {@link ComponentName}. */ ComponentName getComponentName(); } /** * Interface used to receive the changed of {@link android.telecom.ConnectionService} that * ConnectionServiceFocusManager cares about. */ public interface ConnectionServiceFocusListener { /** * Calls when {@link android.telecom.ConnectionService} has released the call resource. This * usually happen after the {@link android.telecom.ConnectionService} lost the focus. * * @param connectionServiceFocus the {@link android.telecom.ConnectionService} that released * the call resources. */ void onConnectionServiceReleased(ConnectionServiceFocus connectionServiceFocus); /** * Calls when {@link android.telecom.ConnectionService} is disconnected. * * @param connectionServiceFocus the {@link android.telecom.ConnectionService} which is * disconnected. */ void onConnectionServiceDeath(ConnectionServiceFocus connectionServiceFocus); } /** * Interface define to expose few information of {@link Call} that ConnectionServiceFocusManager * cares about. */ public interface CallFocus { /** * Returns the ConnectionService associated with the call. */ ConnectionServiceFocus getConnectionServiceWrapper(); /** * Returns the state of the call. * * @see {@link CallState} */ int getState(); /** * @return {@code True} if this call can receive focus, {@code false} otherwise. */ boolean isFocusable(); } /** Interface define a call back for focus request event. */ public interface RequestFocusCallback { /** * Invokes after the focus request is done. * * @param call the call associated with the focus request. */ void onRequestFocusDone(CallFocus call); } /** * Interface define to allow the ConnectionServiceFocusManager to communicate with * {@link CallsManager}. */ public interface CallsManagerRequester { /** * Requests {@link CallsManager} to disconnect a {@link ConnectionServiceFocus}. This * usually happen when the connection service doesn't respond to focus lost event. */ void releaseConnectionService(ConnectionServiceFocus connectionService); /** * Sets the {@link com.android.server.telecom.CallsManager.CallsManagerListener} to listen * the call event that ConnectionServiceFocusManager cares about. */ void setCallsManagerListener(CallsManager.CallsManagerListener listener); } private static final int[] PRIORITY_FOCUS_CALL_STATE = new int[] { CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING, CallState.AUDIO_PROCESSING }; private static final int MSG_REQUEST_FOCUS = 1; private static final int MSG_RELEASE_CONNECTION_FOCUS = 2; private static final int MSG_RELEASE_FOCUS_TIMEOUT = 3; private static final int MSG_CONNECTION_SERVICE_DEATH = 4; private static final int MSG_ADD_CALL = 5; private static final int MSG_REMOVE_CALL = 6; private static final int MSG_CALL_STATE_CHANGED = 7; @VisibleForTesting public static final int RELEASE_FOCUS_TIMEOUT_MS = 5000; private final List mCalls; private final CallsManagerListenerBase mCallsManagerListener = new CallsManagerListenerBase() { @Override public void onCallAdded(Call call) { if (callShouldBeIgnored(call)) { return; } mEventHandler .obtainMessage(MSG_ADD_CALL, new MessageArgs( Log.createSubsession(), "CSFM.oCA", call)) .sendToTarget(); } @Override public void onCallRemoved(Call call) { if (callShouldBeIgnored(call)) { return; } mEventHandler .obtainMessage(MSG_REMOVE_CALL, new MessageArgs( Log.createSubsession(), "CSFM.oCR", call)) .sendToTarget(); } @Override public void onCallStateChanged(Call call, int oldState, int newState) { if (callShouldBeIgnored(call)) { return; } mEventHandler .obtainMessage(MSG_CALL_STATE_CHANGED, oldState, newState, new MessageArgs( Log.createSubsession(), "CSFM.oCSS", call)) .sendToTarget(); } @Override public void onExternalCallChanged(Call call, boolean isExternalCall) { if (isExternalCall) { mEventHandler .obtainMessage(MSG_REMOVE_CALL, new MessageArgs( Log.createSubsession(), "CSFM.oECC", call)) .sendToTarget(); } else { mEventHandler .obtainMessage(MSG_ADD_CALL, new MessageArgs( Log.createSubsession(), "CSFM.oECC", call)) .sendToTarget(); } } boolean callShouldBeIgnored(Call call) { return call.isExternalCall(); } }; private final ConnectionServiceFocusListener mConnectionServiceFocusListener = new ConnectionServiceFocusListener() { @Override public void onConnectionServiceReleased( ConnectionServiceFocus connectionServiceFocus) { mEventHandler .obtainMessage(MSG_RELEASE_CONNECTION_FOCUS, new MessageArgs( Log.createSubsession(), "CSFM.oCSR", connectionServiceFocus)) .sendToTarget(); } @Override public void onConnectionServiceDeath( ConnectionServiceFocus connectionServiceFocus) { mEventHandler .obtainMessage(MSG_CONNECTION_SERVICE_DEATH, new MessageArgs( Log.createSubsession(), "CSFM.oCSD", connectionServiceFocus)) .sendToTarget(); } }; private ConnectionServiceFocus mCurrentFocus; private CallFocus mCurrentFocusCall; private CallsManagerRequester mCallsManagerRequester; private FocusRequest mCurrentFocusRequest; private FocusManagerHandler mEventHandler; public ConnectionServiceFocusManager( CallsManagerRequester callsManagerRequester) { mCallsManagerRequester = callsManagerRequester; mCallsManagerRequester.setCallsManagerListener(mCallsManagerListener); HandlerThread handlerThread = new HandlerThread(TAG); handlerThread.start(); mEventHandler = new FocusManagerHandler(handlerThread.getLooper()); mCalls = new ArrayList<>(); } /** * Requests the call focus for the given call. The {@code callback} will be invoked once * the request is done. * @param focus the call need to be focus. * @param callback the callback associated with this request. */ public void requestFocus(CallFocus focus, RequestFocusCallback callback) { mEventHandler.obtainMessage(MSG_REQUEST_FOCUS, new MessageArgs( Log.createSubsession(), "CSFM.rF", new FocusRequest(focus, callback))) .sendToTarget(); } /** * Returns the current focus call. The {@link android.telecom.ConnectionService} of the focus * call is the current connection service focus. Also the state of the focus call must be one * of {@link #PRIORITY_FOCUS_CALL_STATE}. */ public @Nullable CallFocus getCurrentFocusCall() { if (mEventHandler.getLooper().isCurrentThread()) { // return synchronously if we're on the same thread. return mCurrentFocusCall; } final BlockingQueue> currentFocusedCallQueue = new LinkedBlockingQueue<>(1); mEventHandler.post(() -> { currentFocusedCallQueue.offer( mCurrentFocusCall == null ? Optional.empty() : Optional.of(mCurrentFocusCall)); }); try { Optional syncCallFocus = currentFocusedCallQueue.poll( GET_CURRENT_FOCUS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); if (syncCallFocus != null) { return syncCallFocus.orElse(null); } else { Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly" + " inaccurate result"); return mCurrentFocusCall; } } catch (InterruptedException e) { Log.w(TAG, "Interrupted when waiting for synchronous current focus." + " Returning possibly inaccurate result."); return mCurrentFocusCall; } } /** Returns the current connection service focus. */ public ConnectionServiceFocus getCurrentFocusConnectionService() { return mCurrentFocus; } @VisibleForTesting public Handler getHandler() { return mEventHandler; } @VisibleForTesting public List getAllCall() { return mCalls; } private void updateConnectionServiceFocus(ConnectionServiceFocus connSvrFocus) { if (!Objects.equals(mCurrentFocus, connSvrFocus)) { if (connSvrFocus != null) { connSvrFocus.setConnectionServiceFocusListener(mConnectionServiceFocusListener); connSvrFocus.connectionServiceFocusGained(); } mCurrentFocus = connSvrFocus; Log.d(this, "updateConnectionServiceFocus connSvr = %s", connSvrFocus); } } private void updateCurrentFocusCall() { mCurrentFocusCall = null; if (mCurrentFocus == null) { return; } List calls = mCalls .stream() .filter(call -> mCurrentFocus.equals(call.getConnectionServiceWrapper()) && call.isFocusable()) .collect(Collectors.toList()); for (int i = 0; i < PRIORITY_FOCUS_CALL_STATE.length; i++) { for (CallFocus call : calls) { if (call.getState() == PRIORITY_FOCUS_CALL_STATE[i]) { mCurrentFocusCall = call; Log.d(this, "updateCurrentFocusCall %s", mCurrentFocusCall); return; } } } Log.d(this, "updateCurrentFocusCall = null"); } private void onRequestFocusDone(FocusRequest focusRequest) { if (focusRequest.callback != null) { focusRequest.callback.onRequestFocusDone(focusRequest.call); } } private void handleRequestFocus(FocusRequest focusRequest) { Log.i(this, "handleRequestFocus req = %s", focusRequest); if (mCurrentFocus == null || mCurrentFocus.equals(focusRequest.call.getConnectionServiceWrapper())) { updateConnectionServiceFocus(focusRequest.call.getConnectionServiceWrapper()); updateCurrentFocusCall(); onRequestFocusDone(focusRequest); } else { mCurrentFocus.connectionServiceFocusLost(); mCurrentFocusRequest = focusRequest; Message msg = mEventHandler.obtainMessage( MSG_RELEASE_FOCUS_TIMEOUT, new MessageArgs( Log.createSubsession(), "CSFM.hRF", focusRequest)); mEventHandler.sendMessageDelayed(msg, RELEASE_FOCUS_TIMEOUT_MS); } } private void handleReleasedFocus(ConnectionServiceFocus connectionServiceFocus) { Log.d(this, "handleReleasedFocus connSvr = %s", connectionServiceFocus); // The ConnectionService can call onConnectionServiceFocusReleased even if it's not the // current focus connection service, nothing will be changed in this case. if (Objects.equals(mCurrentFocus, connectionServiceFocus)) { mEventHandler.removeMessages(MSG_RELEASE_FOCUS_TIMEOUT); ConnectionServiceFocus newCSF = null; if (mCurrentFocusRequest != null) { newCSF = mCurrentFocusRequest.call.getConnectionServiceWrapper(); } updateConnectionServiceFocus(newCSF); updateCurrentFocusCall(); if (mCurrentFocusRequest != null) { onRequestFocusDone(mCurrentFocusRequest); mCurrentFocusRequest = null; } } } private void handleReleasedFocusTimeout(FocusRequest focusRequest) { Log.d(this, "handleReleasedFocusTimeout req = %s", focusRequest); mCallsManagerRequester.releaseConnectionService(mCurrentFocus); updateConnectionServiceFocus(focusRequest.call.getConnectionServiceWrapper()); updateCurrentFocusCall(); onRequestFocusDone(focusRequest); mCurrentFocusRequest = null; } private void handleConnectionServiceDeath(ConnectionServiceFocus connectionServiceFocus) { Log.d(this, "handleConnectionServiceDeath %s", connectionServiceFocus); if (Objects.equals(connectionServiceFocus, mCurrentFocus)) { updateConnectionServiceFocus(null); updateCurrentFocusCall(); } } private void handleAddedCall(CallFocus call) { Log.d(this, "handleAddedCall %s", call); if (!mCalls.contains(call)) { mCalls.add(call); } if (Objects.equals(mCurrentFocus, call.getConnectionServiceWrapper())) { updateCurrentFocusCall(); } } private void handleRemovedCall(CallFocus call) { Log.d(this, "handleRemovedCall %s", call); mCalls.remove(call); if (call.equals(mCurrentFocusCall)) { updateCurrentFocusCall(); } } private void handleCallStateChanged(CallFocus call, int oldState, int newState) { Log.d(this, "handleCallStateChanged %s, oldState = %d, newState = %d", call, oldState, newState); if (mCalls.contains(call) && Objects.equals(mCurrentFocus, call.getConnectionServiceWrapper())) { updateCurrentFocusCall(); } } private final class FocusManagerHandler extends Handler { FocusManagerHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { Session session = ((MessageArgs) msg.obj).logSession; String shortName = ((MessageArgs) msg.obj).shortName; if (TextUtils.isEmpty(shortName)) { shortName = "hM"; } Log.continueSession(session, shortName); Object msgObj = ((MessageArgs) msg.obj).obj; try { switch (msg.what) { case MSG_REQUEST_FOCUS: handleRequestFocus((FocusRequest) msgObj); break; case MSG_RELEASE_CONNECTION_FOCUS: handleReleasedFocus((ConnectionServiceFocus) msgObj); break; case MSG_RELEASE_FOCUS_TIMEOUT: handleReleasedFocusTimeout((FocusRequest) msgObj); break; case MSG_CONNECTION_SERVICE_DEATH: handleConnectionServiceDeath((ConnectionServiceFocus) msgObj); break; case MSG_ADD_CALL: handleAddedCall((CallFocus) msgObj); break; case MSG_REMOVE_CALL: handleRemovedCall((CallFocus) msgObj); break; case MSG_CALL_STATE_CHANGED: handleCallStateChanged((CallFocus) msgObj, msg.arg1, msg.arg2); break; } } finally { Log.endSession(); } } } private static final class FocusRequest { CallFocus call; @Nullable RequestFocusCallback callback; FocusRequest(CallFocus call, RequestFocusCallback callback) { this.call = call; this.callback = callback; } } private static final class MessageArgs { Session logSession; String shortName; Object obj; MessageArgs(Session logSession, String shortName, Object obj) { this.logSession = logSession; this.shortName = shortName; this.obj = obj; } } }