blob: df1140304e51424b95a55149ad3eca88b74f73f1 [file] [log] [blame]
/*
* 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.server.telecom;
import android.Manifest;
import android.app.AppOpsManager;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecom.Connection;
import android.telecom.InCallService;
import android.telecom.Log;
import android.telecom.VideoProfile;
import android.text.TextUtils;
import android.view.Surface;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IVideoCallback;
import com.android.internal.telecom.IVideoProvider;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Proxies video provider messages from {@link InCallService.VideoCall}
* implementations to the underlying {@link Connection.VideoProvider} implementation. Also proxies
* callbacks from the {@link Connection.VideoProvider} to {@link InCallService.VideoCall}
* implementations.
*
* Also provides a means for Telecom to send and receive these messages.
*/
public class VideoProviderProxy extends Connection.VideoProvider {
/**
* Listener for Telecom components interested in callbacks from the video provider.
*/
public interface Listener {
void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
void onSetCamera(Call call, String cameraId);
}
/**
* Set of listeners on this VideoProviderProxy.
*
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
* load factor before resizing, 1 means we only expect a single thread to
* access the map so make only a single shard
*/
private final Set<Listener> mListeners = Collections.newSetFromMap(
new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
/** The TelecomSystem SyncRoot used for synchronized operations. */
private final TelecomSystem.SyncRoot mLock;
/**
* The {@link android.telecom.Connection.VideoProvider} implementation residing with the
* {@link android.telecom.ConnectionService} which is being wrapped by this
* {@link VideoProviderProxy}.
*/
private final IVideoProvider mConectionServiceVideoProvider;
/**
* Binder used to bind to the {@link android.telecom.ConnectionService}'s
* {@link com.android.internal.telecom.IVideoCallback}.
*/
private final VideoCallListenerBinder mVideoCallListenerBinder;
/**
* The Telecom {@link Call} this {@link VideoProviderProxy} is associated with.
*/
private Call mCall;
/**
* Interface providing access to the currently logged in user.
*/
private CurrentUserProxy mCurrentUserProxy;
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
mConectionServiceVideoProvider.asBinder().unlinkToDeath(this, 0);
}
};
/**
* Creates a new instance of the {@link VideoProviderProxy}, binding it to the passed in
* {@code videoProvider} residing with the {@link android.telecom.ConnectionService}.
*
*
* @param lock
* @param videoProvider The {@link android.telecom.ConnectionService}'s video provider.
* @param call The current call.
* @throws RemoteException Remote exception.
*/
public VideoProviderProxy(TelecomSystem.SyncRoot lock,
IVideoProvider videoProvider, Call call, CurrentUserProxy currentUserProxy)
throws RemoteException {
super(Looper.getMainLooper());
mLock = lock;
mConectionServiceVideoProvider = videoProvider;
mConectionServiceVideoProvider.asBinder().linkToDeath(mDeathRecipient, 0);
mVideoCallListenerBinder = new VideoCallListenerBinder();
mConectionServiceVideoProvider.addVideoCallback(mVideoCallListenerBinder);
mCall = call;
mCurrentUserProxy = currentUserProxy;
}
public void clearVideoCallback() {
try {
mConectionServiceVideoProvider.removeVideoCallback(mVideoCallListenerBinder);
} catch (RemoteException e) {
}
}
@VisibleForTesting
public VideoCallListenerBinder getVideoCallListenerBinder() {
return mVideoCallListenerBinder;
}
/**
* IVideoCallback stub implementation. An instance of this class receives callbacks from the
* {@code ConnectionService}'s video provider.
*/
public final class VideoCallListenerBinder extends IVideoCallback.Stub {
/**
* Proxies a request from the {@link #mConectionServiceVideoProvider} to the
* {@link InCallService} when a session modification request is received.
*
* @param videoProfile The requested video profile.
*/
@Override
public void receiveSessionModifyRequest(VideoProfile videoProfile) {
try {
Log.startSession("VPP.rSMR");
synchronized (mLock) {
logFromVideoProvider("receiveSessionModifyRequest: " + videoProfile);
Log.addEvent(mCall, LogUtils.Events.RECEIVE_VIDEO_REQUEST,
VideoProfile.videoStateToString(videoProfile.getVideoState()));
mCall.getAnalytics().addVideoEvent(
Analytics.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST,
videoProfile.getVideoState());
if ((!mCall.isVideoCallingSupportedByPhoneAccount()
|| !mCall.isLocallyVideoCapable())
&& VideoProfile.isVideo(videoProfile.getVideoState())) {
// If video calling is not supported by the phone account, or is not
// locally video capable and we receive a request to upgrade to video,
// automatically reject it without informing the InCallService.
Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE,
"video not supported");
VideoProfile responseProfile = new VideoProfile(
VideoProfile.STATE_AUDIO_ONLY);
try {
mConectionServiceVideoProvider.sendSessionModifyResponse(
responseProfile);
} catch (RemoteException e) {
}
// Don't want to inform listeners of the request as we've just rejected it.
return;
}
// Inform other Telecom components of the session modification request.
for (Listener listener : mListeners) {
listener.onSessionModifyRequestReceived(mCall, videoProfile);
}
VideoProviderProxy.this.receiveSessionModifyRequest(videoProfile);
}
} finally {
Log.endSession();
}
}
/**
* Proxies a request from the {@link #mConectionServiceVideoProvider} to the
* {@link InCallService} when a session modification response is received.
*
* @param status The status of the response.
* @param requestProfile The requested video profile.
* @param responseProfile The response video profile.
*/
@Override
public void receiveSessionModifyResponse(int status, VideoProfile requestProfile,
VideoProfile responseProfile) {
logFromVideoProvider("receiveSessionModifyResponse: status=" + status +
" requestProfile=" + requestProfile + " responseProfile=" + responseProfile);
String eventMessage = "Status Code : " + status + " Video State: " +
(responseProfile != null ? responseProfile.getVideoState() : "null");
Log.addEvent(mCall, LogUtils.Events.RECEIVE_VIDEO_RESPONSE, eventMessage);
synchronized (mLock) {
if (status == Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
mCall.getAnalytics().addVideoEvent(
Analytics.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE,
responseProfile == null ?
VideoProfile.STATE_AUDIO_ONLY :
responseProfile.getVideoState());
}
VideoProviderProxy.this.receiveSessionModifyResponse(status, requestProfile,
responseProfile);
}
}
/**
* Proxies a request from the {@link #mConectionServiceVideoProvider} to the
* {@link InCallService} when a call session event occurs.
*
* @param event The call session event.
*/
@Override
public void handleCallSessionEvent(int event) {
synchronized (mLock) {
logFromVideoProvider("handleCallSessionEvent: " +
Connection.VideoProvider.sessionEventToString(event));
VideoProviderProxy.this.handleCallSessionEvent(event);
}
}
/**
* Proxies a request from the {@link #mConectionServiceVideoProvider} to the
* {@link InCallService} when the peer dimensions change.
*
* @param width The width of the peer's video.
* @param height The height of the peer's video.
*/
@Override
public void changePeerDimensions(int width, int height) {
synchronized (mLock) {
logFromVideoProvider("changePeerDimensions: width=" + width + " height=" +
height);
VideoProviderProxy.this.changePeerDimensions(width, height);
}
}
/**
* Proxies a request from the {@link #mConectionServiceVideoProvider} to the
* {@link InCallService} when the video quality changes.
*
* @param videoQuality The video quality.
*/
@Override
public void changeVideoQuality(int videoQuality) {
synchronized (mLock) {
logFromVideoProvider("changeVideoQuality: " + videoQuality);
VideoProviderProxy.this.changeVideoQuality(videoQuality);
}
}
/**
* Proxies a request from the {@link #mConectionServiceVideoProvider} to the
* {@link InCallService} when the call data usage changes.
*
* Also tracks the current call data usage on the {@link Call} for use when writing to the
* call log.
*
* @param dataUsage The data usage.
*/
@Override
public void changeCallDataUsage(long dataUsage) {
synchronized (mLock) {
logFromVideoProvider("changeCallDataUsage: " + dataUsage);
VideoProviderProxy.this.setCallDataUsage(dataUsage);
mCall.setCallDataUsage(dataUsage);
}
}
/**
* Proxies a request from the {@link #mConectionServiceVideoProvider} to the
* {@link InCallService} when the camera capabilities change.
*
* @param cameraCapabilities The camera capabilities.
*/
@Override
public void changeCameraCapabilities(VideoProfile.CameraCapabilities cameraCapabilities) {
synchronized (mLock) {
logFromVideoProvider("changeCameraCapabilities: " + cameraCapabilities);
VideoProviderProxy.this.changeCameraCapabilities(cameraCapabilities);
}
}
}
@Override
public void onSetCamera(String cameraId) {
// No-op. We implement the other prototype of onSetCamera so that we can use the calling
// package, uid and pid to verify permission.
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to change the camera.
*
* @param cameraId The id of the camera.
* @param callingPackage The package calling in.
* @param callingUid The UID of the caller.
* @param callingPid The PID of the caller.
* @param targetSdkVersion The target SDK version of the calling InCallService where the camera
* request originated.
*/
@Override
public void onSetCamera(String cameraId, String callingPackage, int callingUid,
int callingPid, int targetSdkVersion) {
synchronized (mLock) {
logFromInCall("setCamera: " + cameraId + " callingPackage=" + callingPackage +
"; callingUid=" + callingUid);
if (!TextUtils.isEmpty(cameraId)) {
if (!canUseCamera(mCall.getContext(), callingPackage, callingUid, callingPid)) {
// Calling app is not permitted to use the camera. Ignore the request and send
// back a call session event indicating the error.
Log.i(this, "onSetCamera: camera permission denied; package=%s, uid=%d, "
+ "pid=%d, targetSdkVersion=%d",
callingPackage, callingUid, callingPid, targetSdkVersion);
// API 26 introduces a new camera permission error we can use here since the
// caller supports that API version.
if (targetSdkVersion > Build.VERSION_CODES.N_MR1) {
VideoProviderProxy.this.handleCallSessionEvent(
Connection.VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR);
} else {
VideoProviderProxy.this.handleCallSessionEvent(
Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE);
}
return;
}
}
// Inform other Telecom components of the change in camera status.
for (Listener listener : mListeners) {
listener.onSetCamera(mCall, cameraId);
}
try {
mConectionServiceVideoProvider.setCamera(cameraId, callingPackage,
targetSdkVersion);
} catch (RemoteException e) {
VideoProviderProxy.this.handleCallSessionEvent(
Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE);
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to set the preview surface.
*
* @param surface The surface.
*/
@Override
public void onSetPreviewSurface(Surface surface) {
synchronized (mLock) {
logFromInCall("setPreviewSurface");
try {
mConectionServiceVideoProvider.setPreviewSurface(surface);
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to change the display surface.
*
* @param surface The surface.
*/
@Override
public void onSetDisplaySurface(Surface surface) {
synchronized (mLock) {
logFromInCall("setDisplaySurface");
try {
mConectionServiceVideoProvider.setDisplaySurface(surface);
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to change the device orientation.
*
* @param rotation The device orientation, in degrees.
*/
@Override
public void onSetDeviceOrientation(int rotation) {
synchronized (mLock) {
logFromInCall("setDeviceOrientation: " + rotation);
try {
mConectionServiceVideoProvider.setDeviceOrientation(rotation);
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to change the camera zoom ratio.
*
* @param value The camera zoom ratio.
*/
@Override
public void onSetZoom(float value) {
synchronized (mLock) {
logFromInCall("setZoom: " + value);
try {
mConectionServiceVideoProvider.setZoom(value);
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to provide a response to a session modification
* request.
*
* @param fromProfile The video properties prior to the request.
* @param toProfile The video properties with the requested changes made.
*/
@Override
public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) {
synchronized (mLock) {
logFromInCall("sendSessionModifyRequest: from=" + fromProfile + " to=" + toProfile);
Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_REQUEST,
VideoProfile.videoStateToString(toProfile.getVideoState()));
if (!VideoProfile.isVideo(fromProfile.getVideoState())
&& VideoProfile.isVideo(toProfile.getVideoState())) {
// Upgrading to video; change to speaker potentially.
mCall.maybeEnableSpeakerForVideoUpgrade(toProfile.getVideoState());
}
mCall.getAnalytics().addVideoEvent(
Analytics.SEND_LOCAL_SESSION_MODIFY_REQUEST,
toProfile.getVideoState());
try {
mConectionServiceVideoProvider.sendSessionModifyRequest(fromProfile, toProfile);
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to send a session modification request.
*
* @param responseProfile The response connection video properties.
*/
@Override
public void onSendSessionModifyResponse(VideoProfile responseProfile) {
synchronized (mLock) {
logFromInCall("sendSessionModifyResponse: " + responseProfile);
Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE,
VideoProfile.videoStateToString(responseProfile.getVideoState()));
mCall.getAnalytics().addVideoEvent(
Analytics.SEND_LOCAL_SESSION_MODIFY_RESPONSE,
responseProfile.getVideoState());
try {
mConectionServiceVideoProvider.sendSessionModifyResponse(responseProfile);
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to request the camera capabilities.
*/
@Override
public void onRequestCameraCapabilities() {
synchronized (mLock) {
logFromInCall("requestCameraCapabilities");
try {
mConectionServiceVideoProvider.requestCameraCapabilities();
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to request the connection data usage.
*/
@Override
public void onRequestConnectionDataUsage() {
synchronized (mLock) {
logFromInCall("requestCallDataUsage");
try {
mConectionServiceVideoProvider.requestCallDataUsage();
} catch (RemoteException e) {
}
}
}
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to set the pause image.
*
* @param uri URI of image to display.
*/
@Override
public void onSetPauseImage(Uri uri) {
synchronized (mLock) {
logFromInCall("setPauseImage: " + uri);
try {
mConectionServiceVideoProvider.setPauseImage(uri);
} catch (RemoteException e) {
}
}
}
/**
* Add a listener to this {@link VideoProviderProxy}.
*
* @param listener The listener.
*/
public void addListener(Listener listener) {
mListeners.add(listener);
}
/**
* Remove a listener from this {@link VideoProviderProxy}.
*
* @param listener The listener.
*/
public void removeListener(Listener listener) {
if (listener != null) {
mListeners.remove(listener);
}
}
/**
* Logs a message originating from the {@link InCallService}.
*
* @param toLog The message to log.
*/
private void logFromInCall(String toLog) {
Log.i(this, "IC->VP (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog);
}
/**
* Logs a message originating from the {@link android.telecom.ConnectionService}'s
* {@link Connection.VideoProvider}.
*
* @param toLog The message to log.
*/
private void logFromVideoProvider(String toLog) {
Log.i(this, "VP->IC (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog);
}
/**
* Determines if the caller has permission to use the camera.
*
* @param context The context.
* @param callingPackage The package name of the caller (i.e. Dialer).
* @param callingUid The UID of the caller.
* @param callingPid The PID of the caller.
* @return {@code true} if the calling uid and package can use the camera, {@code false}
* otherwise.
*/
private boolean canUseCamera(Context context, String callingPackage, int callingUid,
int callingPid) {
UserHandle callingUser = UserHandle.getUserHandleForUid(callingUid);
UserHandle currentUserHandle = mCurrentUserProxy.getCurrentUserHandle();
if (currentUserHandle != null && !currentUserHandle.equals(callingUser)) {
Log.w(this, "canUseCamera attempt to user camera by background user.");
return false;
}
try {
context.enforcePermission(Manifest.permission.CAMERA, callingPid, callingUid,
"Camera permission required.");
} catch (SecurityException se) {
return false;
}
AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(
Context.APP_OPS_SERVICE);
try {
// Some apps that have the permission can be restricted via app ops.
return appOpsManager != null && appOpsManager.noteOp(AppOpsManager.OP_CAMERA,
callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED;
} catch (SecurityException se) {
Log.w(this, "canUseCamera got appOpps Exception " + se.toString());
return false;
}
}
}