1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.incallui.answer.impl;
18 
19 import android.content.Context;
20 import android.hardware.camera2.CameraAccessException;
21 import android.hardware.camera2.CameraCaptureSession;
22 import android.hardware.camera2.CameraCharacteristics;
23 import android.hardware.camera2.CameraDevice;
24 import android.hardware.camera2.CameraDevice.StateCallback;
25 import android.hardware.camera2.CameraManager;
26 import android.hardware.camera2.CameraMetadata;
27 import android.hardware.camera2.CaptureRequest;
28 import android.hardware.camera2.params.StreamConfigurationMap;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.Nullable;
31 import android.support.v4.app.Fragment;
32 import android.util.Size;
33 import android.view.Surface;
34 import android.view.SurfaceHolder;
35 import android.view.SurfaceView;
36 import android.view.View;
37 import com.android.dialer.common.Assert;
38 import com.android.dialer.common.LogUtil;
39 import com.android.incallui.video.protocol.VideoCallScreen;
40 import java.util.Arrays;
41 
42 /**
43  * Shows the local preview for the incoming video call or video upgrade request. This class is used
44  * for RCS Video Share where we need to open the camera preview ourselves. For IMS Video the camera
45  * is managed by the modem, see {@link AnswerVideoCallScreen}.
46  */
47 public class SelfManagedAnswerVideoCallScreen extends StateCallback implements VideoCallScreen {
48 
49   private static final int MAX_WIDTH = 1920;
50   private static final float ASPECT_TOLERANCE = 0.1f;
51   private static final float TARGET_ASPECT = 16.f / 9.f;
52 
53   @NonNull private final String callId;
54   @NonNull private final Fragment fragment;
55   @NonNull private final FixedAspectSurfaceView surfaceView;
56   private final Context context;
57 
58   private String cameraId;
59   private CameraDevice camera;
60   private CaptureRequest.Builder captureRequestBuilder;
61 
SelfManagedAnswerVideoCallScreen( @onNull String callId, @NonNull Fragment fragment, @NonNull View view)62   public SelfManagedAnswerVideoCallScreen(
63       @NonNull String callId, @NonNull Fragment fragment, @NonNull View view) {
64     this.callId = Assert.isNotNull(callId);
65     this.fragment = Assert.isNotNull(fragment);
66     this.context = Assert.isNotNull(fragment.getContext());
67 
68     surfaceView =
69         Assert.isNotNull(
70             (FixedAspectSurfaceView) view.findViewById(R.id.incoming_preview_surface_view));
71     surfaceView.setVisibility(View.VISIBLE);
72     view.findViewById(R.id.incoming_preview_texture_view_overlay).setVisibility(View.VISIBLE);
73     view.setBackgroundColor(0xff000000);
74   }
75 
76   @Override
onVideoScreenStart()77   public void onVideoScreenStart() {
78     openCamera();
79   }
80 
81   @Override
onVideoScreenStop()82   public void onVideoScreenStop() {
83     closeCamera();
84   }
85 
86   @Override
showVideoViews( boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld)87   public void showVideoViews(
88       boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {}
89 
90   @Override
onLocalVideoDimensionsChanged()91   public void onLocalVideoDimensionsChanged() {}
92 
93   @Override
onLocalVideoOrientationChanged()94   public void onLocalVideoOrientationChanged() {}
95 
96   @Override
onRemoteVideoDimensionsChanged()97   public void onRemoteVideoDimensionsChanged() {}
98 
99   @Override
updateFullscreenAndGreenScreenMode( boolean shouldShowFullscreen, boolean shouldShowGreenScreen)100   public void updateFullscreenAndGreenScreenMode(
101       boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {}
102 
103   @Override
getVideoCallScreenFragment()104   public Fragment getVideoCallScreenFragment() {
105     return fragment;
106   }
107 
108   @Override
getCallId()109   public String getCallId() {
110     return callId;
111   }
112 
113   @Override
onHandoverFromWiFiToLte()114   public void onHandoverFromWiFiToLte() {}
115 
116   /**
117    * Opens the first front facing camera on the device into a {@link SurfaceView} while preserving
118    * aspect ratio.
119    */
openCamera()120   private void openCamera() {
121     CameraManager manager = context.getSystemService(CameraManager.class);
122 
123     StreamConfigurationMap configMap = getFrontFacingCameraSizes(manager);
124     if (configMap == null) {
125       return;
126     }
127 
128     Size previewSize = getOptimalSize(configMap.getOutputSizes(SurfaceHolder.class));
129     LogUtil.i("SelfManagedAnswerVideoCallScreen.openCamera", "Optimal size: " + previewSize);
130     float outputAspect = (float) previewSize.getWidth() / previewSize.getHeight();
131     surfaceView.setAspectRatio(outputAspect);
132     surfaceView.getHolder().setFixedSize(previewSize.getWidth(), previewSize.getHeight());
133 
134     try {
135       manager.openCamera(cameraId, this, null);
136     } catch (CameraAccessException e) {
137       LogUtil.e("SelfManagedAnswerVideoCallScreen.openCamera", "failed to open camera", e);
138     }
139   }
140 
141   @Nullable
getFrontFacingCameraSizes(CameraManager manager)142   private StreamConfigurationMap getFrontFacingCameraSizes(CameraManager manager) {
143     String[] cameraIds;
144     try {
145       cameraIds = manager.getCameraIdList();
146     } catch (CameraAccessException e) {
147       LogUtil.e(
148           "SelfManagedAnswerVideoCallScreen.getFrontFacingCameraSizes",
149           "failed to get camera ids",
150           e);
151       return null;
152     }
153 
154     for (String cameraId : cameraIds) {
155       CameraCharacteristics characteristics;
156       try {
157         characteristics = manager.getCameraCharacteristics(cameraId);
158       } catch (CameraAccessException e) {
159         LogUtil.e(
160             "SelfManagedAnswerVideoCallScreen.getFrontFacingCameraSizes",
161             "failed to get camera characteristics",
162             e);
163         continue;
164       }
165 
166       if (characteristics.get(CameraCharacteristics.LENS_FACING)
167           != CameraCharacteristics.LENS_FACING_FRONT) {
168         continue;
169       }
170 
171       StreamConfigurationMap configMap =
172           characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
173       if (configMap == null) {
174         continue;
175       }
176 
177       this.cameraId = cameraId;
178       return configMap;
179     }
180     LogUtil.e(
181         "SelfManagedAnswerVideoCallScreen.getFrontFacingCameraSizes", "No valid configurations.");
182     return null;
183   }
184 
185   /**
186    * Given an array of {@link Size}s, tries to find the largest Size such that the aspect ratio of
187    * the returned size is within {@code ASPECT_TOLERANCE} of {@code TARGET_ASPECT}. This is useful
188    * because it provides us with an adequate size/camera resolution that will experience the least
189    * stretching from our fullscreen UI that doesn't match any of the camera sizes.
190    */
getOptimalSize(Size[] outputSizes)191   private static Size getOptimalSize(Size[] outputSizes) {
192     Size bestCandidateSize = outputSizes[0];
193     float bestCandidateAspect =
194         (float) bestCandidateSize.getWidth() / bestCandidateSize.getHeight();
195 
196     for (Size candidateSize : outputSizes) {
197       if (candidateSize.getWidth() < MAX_WIDTH) {
198         float candidateAspect = (float) candidateSize.getWidth() / candidateSize.getHeight();
199         boolean isGoodCandidateAspect =
200             Math.abs(candidateAspect - TARGET_ASPECT) < ASPECT_TOLERANCE;
201         boolean isGoodOutputAspect =
202             Math.abs(bestCandidateAspect - TARGET_ASPECT) < ASPECT_TOLERANCE;
203 
204         if ((isGoodCandidateAspect && !isGoodOutputAspect)
205             || candidateSize.getWidth() > bestCandidateSize.getWidth()) {
206           bestCandidateSize = candidateSize;
207           bestCandidateAspect = candidateAspect;
208         }
209       }
210     }
211     return bestCandidateSize;
212   }
213 
214   @Override
215   public void onOpened(CameraDevice camera) {
216     LogUtil.i("SelfManagedAnswerVideoCallScreen.opOpened", "camera opened.");
217     this.camera = camera;
218     Surface surface = surfaceView.getHolder().getSurface();
219     try {
220       captureRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
221       captureRequestBuilder.addTarget(surface);
222       camera.createCaptureSession(Arrays.asList(surface), new CaptureSessionCallback(), null);
223     } catch (CameraAccessException e) {
224       LogUtil.e(
225           "SelfManagedAnswerVideoCallScreen.createCameraPreview", "failed to create preview", e);
226     }
227   }
228 
229   @Override
230   public void onDisconnected(CameraDevice camera) {
231     closeCamera();
232   }
233 
234   @Override
235   public void onError(CameraDevice camera, int error) {
236     closeCamera();
237   }
238 
239   private void closeCamera() {
240     if (camera != null) {
241       camera.close();
242       camera = null;
243     }
244   }
245 
246   private class CaptureSessionCallback extends CameraCaptureSession.StateCallback {
247 
248     @Override
249     public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
250       LogUtil.i(
251           "SelfManagedAnswerVideoCallScreen.onConfigured", "camera capture session configured.");
252       // The camera is already closed.
253       if (camera == null) {
254         return;
255       }
256 
257       // When the session is ready, we start displaying the preview.
258       captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
259       try {
260         cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
261       } catch (CameraAccessException e) {
262         LogUtil.e("CaptureSessionCallback.onConfigured", "failed to configure", e);
263       }
264     }
265 
266     @Override
267     public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
268       LogUtil.e("CaptureSessionCallback.onConfigureFailed", "failed to configure");
269     }
270   }
271 }
272