/* * Copyright (C) 2014 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 android.telecom; import android.compat.annotation.UnsupportedAppUsage; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.telecom.InCallService.VideoCall; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.telecom.IVideoCallback; import com.android.internal.telecom.IVideoProvider; import java.util.NoSuchElementException; /** * Implementation of a Video Call, which allows InCallUi to communicate commands to the underlying * {@link Connection.VideoProvider}, and direct callbacks from the * {@link Connection.VideoProvider} to the appropriate {@link VideoCall.Listener}. * * {@hide} */ public class VideoCallImpl extends VideoCall { private final IVideoProvider mVideoProvider; private final VideoCallListenerBinder mBinder; private VideoCall.Callback mCallback; private int mVideoQuality = VideoProfile.QUALITY_UNKNOWN; private int mVideoState = VideoProfile.STATE_AUDIO_ONLY; private final String mCallingPackageName; private int mTargetSdkVersion; private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { try { mVideoProvider.asBinder().unlinkToDeath(this, 0); } catch (NoSuchElementException nse) { // Already unlinked in destroy below. } } }; /** * IVideoCallback stub implementation. */ private final class VideoCallListenerBinder extends IVideoCallback.Stub { @Override public void receiveSessionModifyRequest(VideoProfile videoProfile) { if (mHandler == null) { return; } mHandler.obtainMessage(MessageHandler.MSG_RECEIVE_SESSION_MODIFY_REQUEST, videoProfile).sendToTarget(); } @Override public void receiveSessionModifyResponse(int status, VideoProfile requestProfile, VideoProfile responseProfile) { if (mHandler == null) { return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = status; args.arg2 = requestProfile; args.arg3 = responseProfile; mHandler.obtainMessage(MessageHandler.MSG_RECEIVE_SESSION_MODIFY_RESPONSE, args) .sendToTarget(); } @Override public void handleCallSessionEvent(int event) { if (mHandler == null) { return; } mHandler.obtainMessage(MessageHandler.MSG_HANDLE_CALL_SESSION_EVENT, event) .sendToTarget(); } @Override public void changePeerDimensions(int width, int height) { if (mHandler == null) { return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = width; args.arg2 = height; mHandler.obtainMessage(MessageHandler.MSG_CHANGE_PEER_DIMENSIONS, args).sendToTarget(); } @Override public void changeVideoQuality(int videoQuality) { if (mHandler == null) { return; } mHandler.obtainMessage(MessageHandler.MSG_CHANGE_VIDEO_QUALITY, videoQuality, 0) .sendToTarget(); } @Override public void changeCallDataUsage(long dataUsage) { if (mHandler == null) { return; } mHandler.obtainMessage(MessageHandler.MSG_CHANGE_CALL_DATA_USAGE, dataUsage) .sendToTarget(); } @Override public void changeCameraCapabilities(VideoProfile.CameraCapabilities cameraCapabilities) { if (mHandler == null) { return; } mHandler.obtainMessage(MessageHandler.MSG_CHANGE_CAMERA_CAPABILITIES, cameraCapabilities).sendToTarget(); } } /** Default handler used to consolidate binder method calls onto a single thread. */ private final class MessageHandler extends Handler { private static final int MSG_RECEIVE_SESSION_MODIFY_REQUEST = 1; private static final int MSG_RECEIVE_SESSION_MODIFY_RESPONSE = 2; private static final int MSG_HANDLE_CALL_SESSION_EVENT = 3; private static final int MSG_CHANGE_PEER_DIMENSIONS = 4; private static final int MSG_CHANGE_CALL_DATA_USAGE = 5; private static final int MSG_CHANGE_CAMERA_CAPABILITIES = 6; private static final int MSG_CHANGE_VIDEO_QUALITY = 7; public MessageHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { if (mCallback == null) { return; } SomeArgs args; switch (msg.what) { case MSG_RECEIVE_SESSION_MODIFY_REQUEST: mCallback.onSessionModifyRequestReceived((VideoProfile) msg.obj); break; case MSG_RECEIVE_SESSION_MODIFY_RESPONSE: args = (SomeArgs) msg.obj; try { int status = (int) args.arg1; VideoProfile requestProfile = (VideoProfile) args.arg2; VideoProfile responseProfile = (VideoProfile) args.arg3; mCallback.onSessionModifyResponseReceived( status, requestProfile, responseProfile); } finally { args.recycle(); } break; case MSG_HANDLE_CALL_SESSION_EVENT: mCallback.onCallSessionEvent((int) msg.obj); break; case MSG_CHANGE_PEER_DIMENSIONS: args = (SomeArgs) msg.obj; try { int width = (int) args.arg1; int height = (int) args.arg2; mCallback.onPeerDimensionsChanged(width, height); } finally { args.recycle(); } break; case MSG_CHANGE_CALL_DATA_USAGE: mCallback.onCallDataUsageChanged((long) msg.obj); break; case MSG_CHANGE_CAMERA_CAPABILITIES: mCallback.onCameraCapabilitiesChanged( (VideoProfile.CameraCapabilities) msg.obj); break; case MSG_CHANGE_VIDEO_QUALITY: mVideoQuality = msg.arg1; mCallback.onVideoQualityChanged(msg.arg1); break; default: break; } } }; private Handler mHandler; VideoCallImpl(IVideoProvider videoProvider, String callingPackageName, int targetSdkVersion) throws RemoteException { mVideoProvider = videoProvider; mVideoProvider.asBinder().linkToDeath(mDeathRecipient, 0); mBinder = new VideoCallListenerBinder(); mVideoProvider.addVideoCallback(mBinder); mCallingPackageName = callingPackageName; setTargetSdkVersion(targetSdkVersion); } @VisibleForTesting public void setTargetSdkVersion(int sdkVersion) { mTargetSdkVersion = sdkVersion; } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 127403196) public void destroy() { unregisterCallback(mCallback); try { mVideoProvider.asBinder().unlinkToDeath(mDeathRecipient, 0); } catch (NoSuchElementException nse) { // Already unlinked in binderDied above. } } /** {@inheritDoc} */ public void registerCallback(VideoCall.Callback callback) { registerCallback(callback, null); } /** {@inheritDoc} */ public void registerCallback(VideoCall.Callback callback, Handler handler) { mCallback = callback; if (handler == null) { mHandler = new MessageHandler(Looper.getMainLooper()); } else { mHandler = new MessageHandler(handler.getLooper()); } } /** {@inheritDoc} */ public void unregisterCallback(VideoCall.Callback callback) { if (callback != mCallback) { return; } mCallback = null; try { mVideoProvider.removeVideoCallback(mBinder); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void setCamera(String cameraId) { try { Log.w(this, "setCamera: cameraId=%s, calling=%s", cameraId, mCallingPackageName); mVideoProvider.setCamera(cameraId, mCallingPackageName, mTargetSdkVersion); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void setPreviewSurface(Surface surface) { try { mVideoProvider.setPreviewSurface(surface); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void setDisplaySurface(Surface surface) { try { mVideoProvider.setDisplaySurface(surface); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void setDeviceOrientation(int rotation) { try { mVideoProvider.setDeviceOrientation(rotation); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void setZoom(float value) { try { mVideoProvider.setZoom(value); } catch (RemoteException e) { } } /** * Sends a session modification request to the video provider. *

* The {@link InCallService} will create the {@code requestProfile} based on the current * video state (i.e. {@link Call.Details#getVideoState()}). It is, however, possible that the * video state maintained by the {@link InCallService} could get out of sync with what is known * by the {@link android.telecom.Connection.VideoProvider}. To remove ambiguity, the * {@link VideoCallImpl} passes along the pre-modify video profile to the {@code VideoProvider} * to ensure it has full context of the requested change. * * @param requestProfile The requested video profile. */ public void sendSessionModifyRequest(VideoProfile requestProfile) { try { VideoProfile originalProfile = new VideoProfile(mVideoState, mVideoQuality); mVideoProvider.sendSessionModifyRequest(originalProfile, requestProfile); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void sendSessionModifyResponse(VideoProfile responseProfile) { try { mVideoProvider.sendSessionModifyResponse(responseProfile); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void requestCameraCapabilities() { try { mVideoProvider.requestCameraCapabilities(); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void requestCallDataUsage() { try { mVideoProvider.requestCallDataUsage(); } catch (RemoteException e) { } } /** {@inheritDoc} */ public void setPauseImage(Uri uri) { try { mVideoProvider.setPauseImage(uri); } catch (RemoteException e) { } } /** * Sets the video state for the current video call. * @param videoState the new video state. */ public void setVideoState(int videoState) { mVideoState = videoState; } /** * Get the video provider binder. * @return the video provider binder. */ public IVideoProvider getVideoProvider() { return mVideoProvider; } }