1 /*
2  * Copyright (C) 2024 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.google.android.car.kitchensink.camera2;
18 
19 import android.hardware.camera2.CameraAccessException;
20 import android.hardware.camera2.CameraCharacteristics;
21 import android.hardware.camera2.CameraManager;
22 import android.os.Bundle;
23 import android.os.Debug;
24 import android.os.Debug.MemoryInfo;
25 import android.os.HandlerThread;
26 import android.os.SystemClock;
27 import android.util.Log;
28 import android.view.SurfaceView;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.ArrayAdapter;
32 import android.widget.Button;
33 import android.widget.CheckBox;
34 import android.widget.LinearLayout;
35 import android.widget.ListView;
36 
37 import androidx.fragment.app.FragmentActivity;
38 
39 import com.google.android.car.kitchensink.R;
40 
41 import java.lang.reflect.Array;
42 import java.text.SimpleDateFormat;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Calendar;
46 import java.util.HashMap;
47 import java.util.List;
48 import java.util.Locale;
49 import java.util.Map;
50 
51 
52 public final class MultiCameraPreviewActivity extends FragmentActivity {
53     private static final String TAG = MultiCameraPreviewActivity.class.getSimpleName();
54     private final List<CameraPreviewManager> mCameraPreviewManagerList = new ArrayList<>();
55     private final List<SurfaceView> mPreviewSurfaceViewList = new ArrayList<>();
56     private final List<CheckBox> mSelectionCheckBoxList = new ArrayList<>();
57     private CameraManager mCameraManager;
58     private ListView mDetailsListView;
59     private String[] mCameraIds;
60     private HandlerThread mSessionHandlerThread;
61     private boolean mIsPreviewStarted;
62     private boolean mIsRecordingStarted;
63     private long mSessionStartTimeMs;
64 
65     @Override
onCreate(Bundle savedInstanceState)66     protected void onCreate(Bundle savedInstanceState) {
67         super.onCreate(savedInstanceState);
68         setContentView(R.layout.camera2_multi_camera_preview_activity);
69 
70         mDetailsListView = (ListView) findViewById(R.id.camera_details_list_view);
71 
72         Button quitButton = (Button) findViewById(R.id.quit_button);
73         quitButton.setOnClickListener(new View.OnClickListener() {
74             @Override
75             public void onClick(View view) {
76                 finish();
77             }
78         });
79 
80         Button startPreviewButton = (Button) findViewById(R.id.start_preview_button);
81         startPreviewButton.setOnClickListener(new View.OnClickListener() {
82             @Override
83             public void onClick(View v) {
84                 startPreviews();
85             }
86         });
87 
88         Button startRecordingButton = (Button) findViewById(R.id.start_recording_button);
89         startRecordingButton.setOnClickListener(new View.OnClickListener() {
90             @Override
91             public void onClick(View v) {
92                 startRecording();
93             }
94         });
95 
96         Button stopButton = (Button) findViewById(R.id.stop_button);
97         stopButton.setOnClickListener(new View.OnClickListener() {
98             @Override
99             public void onClick(View v) {
100                 stopSession();
101             }
102         });
103 
104         mCameraManager = getSystemService(CameraManager.class);
105 
106         mSessionHandlerThread = new HandlerThread(TAG + "_session_thread");
107         mSessionHandlerThread.start();
108 
109         // Create a list of camera managers to handle each camera device and their capture sessions
110         try {
111             mCameraIds = mCameraManager.getCameraIdListNoLazy();
112             if (mCameraIds.length == 0) {
113                 Log.w(TAG, "Camera service reported no cameras connected to device.");
114                 return;
115             }
116             addPreviewCells(); // Adds preview cells with one SurfaceView for every camera
117             for (int camIdx = 0; camIdx < mCameraIds.length; camIdx++) {
118                 Log.i(TAG, "Creating preview for camera with ID " + mCameraIds[camIdx]);
119                 CameraPreviewManager cameraPreviewManager = new CameraPreviewManager(
120                         mCameraIds[camIdx],
121                         mPreviewSurfaceViewList.get(camIdx),
122                         mCameraManager,
123                         mSessionHandlerThread
124                 );
125                 cameraPreviewManager.openCamera();
126                 mCameraPreviewManagerList.add(cameraPreviewManager);
127             }
128         } catch (CameraAccessException e) {
129             Log.e(TAG, "Failed to get camera ID list, got:", e);
130         } catch (Exception e) {
131             Log.e(TAG, "Unable to open camera, got:", e);
132         }
133     }
134 
addPreviewCells()135     private void addPreviewCells() {
136         // Adds preview cells in a square-like grid
137         int numColumns = (int) Math.ceil(Math.sqrt(mCameraIds.length));
138         int numRows = -Math.floorDiv(-mCameraIds.length, numColumns);  //==ceilDiv(nCameras, nCols)
139         int numPadded = numColumns * numRows - mCameraIds.length;
140         int numCellsLastRow = numColumns - numPadded;
141 
142         LinearLayout previewRoot = (LinearLayout) findViewById(R.id.preview_root);
143 
144         // Fill every row completely with preview cells except last row
145         for (int i = 0; i < numRows - 1; i++) {
146             LinearLayout previewRow = (LinearLayout) View.inflate(
147                     this, R.layout.camera2_preview_row, null);
148             previewRoot.addView(previewRow, new LinearLayout.LayoutParams(
149                     ViewGroup.LayoutParams.MATCH_PARENT,
150                     ViewGroup.LayoutParams.MATCH_PARENT,
151                     1f));
152             for (int j = 0; j < numColumns; j++) {
153                 addPreviewSurfaceView(previewRow, i * numColumns + j);
154             }
155         }
156 
157         // Add last row
158         LinearLayout lastPreviewRow = (LinearLayout) (LinearLayout) View.inflate(
159                 this, R.layout.camera2_preview_row, null);
160         previewRoot.addView(lastPreviewRow, new LinearLayout.LayoutParams(
161                 ViewGroup.LayoutParams.MATCH_PARENT,
162                 ViewGroup.LayoutParams.MATCH_PARENT,
163                 1f));
164         for (int j = 0; j < numCellsLastRow; j++) {
165             addPreviewSurfaceView(lastPreviewRow, (numRows - 1) * numColumns + j);
166         }
167         // Pad rest of the row with empty cells
168         for (int j = numCellsLastRow; j < numColumns; j++) {
169             LinearLayout emptyCell = (LinearLayout) View.inflate(
170                     this, R.layout.camera2_preview_empty_cell, null);
171             lastPreviewRow.addView(emptyCell, new LinearLayout.LayoutParams(
172                     ViewGroup.LayoutParams.MATCH_PARENT,
173                     ViewGroup.LayoutParams.MATCH_PARENT,
174                     1f));
175         }
176     }
177 
addPreviewSurfaceView(ViewGroup parent, int camIdx)178     private void addPreviewSurfaceView(ViewGroup parent, int camIdx) {
179         LinearLayout previewLayout = (LinearLayout) View.inflate(
180                 this, R.layout.camera2_preview_cell, null);
181         parent.addView(previewLayout, new LinearLayout.LayoutParams(
182                 ViewGroup.LayoutParams.MATCH_PARENT,
183                 ViewGroup.LayoutParams.MATCH_PARENT,
184                 1f
185         ));
186 
187         Button detailsButton = (Button) previewLayout.findViewById(R.id.details_button);
188         detailsButton.setOnClickListener(new View.OnClickListener() {
189             @Override
190             public void onClick(View v) {
191                 viewCameraCharacteristics(camIdx);
192             }
193         });
194 
195         CheckBox selectionCheckBox = (CheckBox) previewLayout.findViewById(R.id.selection_checkbox);
196         selectionCheckBox.setText(
197                 getString(R.string.camera2_selection_checkbox, mCameraIds[camIdx]));
198         SurfaceView surfaceView = (SurfaceView) previewLayout.findViewById(R.id.preview_surface);
199 
200         mSelectionCheckBoxList.add(selectionCheckBox);
201         mPreviewSurfaceViewList.add(surfaceView);
202     }
203 
startPreviews()204     private void startPreviews() {
205         // Do nothing if a session has already started
206         if (mIsPreviewStarted || mIsRecordingStarted) {
207             Log.i(TAG, "Start preview button pressed when a session is already running.");
208             return;
209         }
210         // Freeze checkbox selections
211         for (CheckBox checkBox : mSelectionCheckBoxList) {
212             checkBox.setEnabled(false);
213         }
214         // Open Cameras and start previews
215         for (int i = 0; i < mCameraIds.length; i++) {
216             if (mSelectionCheckBoxList.get(i).isChecked()) {
217                 mCameraPreviewManagerList.get(i).startPreviewSession();
218             }
219         }
220 
221         // Set Start Time
222         mSessionStartTimeMs = SystemClock.elapsedRealtime();
223 
224         // Set flag
225         mIsPreviewStarted = true;
226     }
227 
startRecording()228     private void startRecording() {
229         // Do nothing if a session has already started
230         if (mIsPreviewStarted || mIsRecordingStarted) {
231             Log.i(TAG, "Start recording button pressed when a session is already running.");
232             return;
233         }
234         // Freeze checkbox selections
235         for (CheckBox checkBox : mSelectionCheckBoxList) {
236             checkBox.setEnabled(false);
237         }
238 
239         // Get file prefix from current time
240         String dateTimeString =
241                 new SimpleDateFormat("yyyy-MM-dd_hh.mm.ss", Locale.US)
242                         .format(Calendar.getInstance().getTime());
243         String filePathPrefix = String.format("%s/camera2_video_%s",
244                 getExternalFilesDir(null), dateTimeString);
245 
246         // Open Cameras and start recording
247         for (int i = 0; i < mCameraIds.length; i++) {
248             if (mSelectionCheckBoxList.get(i).isChecked()) {
249                 mCameraPreviewManagerList.get(i).startRecordingSession(filePathPrefix);
250             }
251         }
252 
253         // Set Start Time
254         mSessionStartTimeMs = SystemClock.elapsedRealtime();
255 
256         // Set flag
257         mIsRecordingStarted = true;
258     }
259 
stopSession()260     private void stopSession() {
261         // Do nothing if no preview has been started
262         if (!mIsPreviewStarted && !mIsRecordingStarted) {
263             Log.i(TAG, "Stop button pressed when no session has been started.");
264             return;
265         }
266 
267         // Unset flags
268         mIsPreviewStarted = false;
269         mIsRecordingStarted = false;
270 
271         // Session memory usage
272         MemoryInfo sessionMemoryInfo = new MemoryInfo();
273         Debug.getMemoryInfo(sessionMemoryInfo);
274 
275         // Get Frame Counts
276         Map<String, Long> frameCountMap = getSessionFrameCounts();
277 
278         // Get End Time
279         long sessionDurationMs = SystemClock.elapsedRealtime() - mSessionStartTimeMs;
280 
281         // View Session Metrics
282         List<String> sessionMetricsInfoText = getSessionMemoryInfoText(sessionMemoryInfo);
283         sessionMetricsInfoText.add("");
284         sessionMetricsInfoText.addAll(getSessionFpsInfoText(frameCountMap, sessionDurationMs));
285         mDetailsListView.setAdapter(new ArrayAdapter<String>(
286                 this, R.layout.camera2_details_list_item, sessionMetricsInfoText));
287 
288         // Stop camera sessions that have been started
289         for (int i = 0; i < mCameraIds.length; i++) {
290             if (mSelectionCheckBoxList.get(i).isChecked()) {
291                 mCameraPreviewManagerList.get(i).stopSession();
292             }
293         }
294 
295         // Un-freeze checkbox selections
296         for (CheckBox checkBox : mSelectionCheckBoxList) {
297             checkBox.setEnabled(true);
298         }
299     }
300 
getSessionFrameCounts()301     private Map<String, Long> getSessionFrameCounts() {
302         Map<String, Long> frameCountMap = new HashMap<>();
303         for (int i = 0; i < mCameraIds.length; i++) {
304             if (mSelectionCheckBoxList.get(i).isChecked()) {
305                 frameCountMap.put(
306                         mCameraIds[i],
307                         mCameraPreviewManagerList.get(i).getFrameCountOfLastSession());
308             }
309         }
310         return frameCountMap;
311     }
312 
getSessionFpsInfoText( Map<String, Long> frameCountMap, Long sessionDurationMs)313     private static List<String> getSessionFpsInfoText(
314             Map<String, Long> frameCountMap, Long sessionDurationMs) {
315         List<String> infoList = new ArrayList<>(List.of("SESSION FPS INFO (Hz)"));
316         for (Map.Entry<String, Long> entry : frameCountMap.entrySet()) {
317             String cameraId = entry.getKey();
318             long frameCount = entry.getValue();
319             infoList.add(String.format(
320                     "Effective FPS of camera %s: %.2f",
321                     cameraId,
322                     (1000.0 * frameCount) / sessionDurationMs));
323         }
324         return infoList;
325     }
326 
getSessionMemoryInfoText(MemoryInfo memInfo)327     private List<String> getSessionMemoryInfoText(MemoryInfo memInfo) {
328         List<String> infoList = new ArrayList<>(List.of("SESSION MEMORY INFO (kB)"));
329         Map<String, String> memStats = memInfo.getMemoryStats();
330         for (Map.Entry<String, String> memStat : memStats.entrySet()) {
331             infoList.add(String.format("%s: %s", memStat.getKey(), memStat.getValue()));
332         }
333         return infoList;
334     }
335 
viewCameraCharacteristics(int cameraIdx)336     private void viewCameraCharacteristics(int cameraIdx) {
337         List<String> detailsList = new ArrayList<>(Arrays.asList(
338                 String.format("CAMERA %s INFO", mCameraIds[cameraIdx])));
339         detailsList.addAll(getCameraCharacteristics(cameraIdx));
340         mDetailsListView.setAdapter(new ArrayAdapter<String>(
341                 this, R.layout.camera2_details_list_item, detailsList));
342     }
343 
getCameraCharacteristics(int cameraIdx)344     private List<String> getCameraCharacteristics(int cameraIdx) {
345         CameraCharacteristics characteristics;
346         try {
347             characteristics = mCameraManager.getCameraCharacteristics(mCameraIds[cameraIdx]);
348         } catch (CameraAccessException e) {
349             Log.e(TAG, String.format("Camera %s disconnected while fetching characteristics.",
350                     mCameraIds[cameraIdx]), e);
351             return Arrays.asList("Camera disconnected...");
352         } catch (IllegalStateException e) {
353             Log.e(TAG, String.format("Attempting to fetch characteristics of unknown camera ID %s.",
354                     mCameraIds[cameraIdx]), e);
355             return Arrays.asList("Invalid camera ID...");
356         }
357         List<CameraCharacteristics.Key<?>> allKeys = characteristics.getKeys();
358         List<String> detailsList = new ArrayList<>();
359         for (CameraCharacteristics.Key<?> key: allKeys) {
360             try {
361                 Object val = characteristics.get(key);
362                 if (val == null) {
363                     detailsList.add(String.format("%s: (null)", key.getName()));
364                 } else if (val.getClass().isArray()) {
365                     Object[] valAsObjectArray = asObjectArray(val);
366                     detailsList.add(String.format("%s: %s", key.getName(),
367                             Arrays.deepToString(valAsObjectArray)));
368                 } else {
369                     detailsList.add(String.format("%s: %s", key.getName(), val));
370                 }
371 
372             } catch (IllegalArgumentException e) {
373                 Log.e(TAG, String.format("Invalid key %s found in camera ID %s", key.getName(),
374                         mCameraIds[cameraIdx]));
375                 detailsList.add(String.format("%s: INVALID KEY", key.getName()));
376             }
377         }
378         return detailsList;
379     }
380 
asObjectArray(Object array)381     private static Object[] asObjectArray(Object array) {
382         int length = Array.getLength(array);
383         Object[] ret = new Object[length];
384         for (int i = 0; i < length; i++) {
385             ret[i] = Array.get(array, i);
386         }
387         return ret;
388     }
389 
390     @Override
onDestroy()391     protected void onDestroy() {
392         for (CameraPreviewManager cameraPreviewManager : mCameraPreviewManagerList) {
393             cameraPreviewManager.closeCamera();
394         }
395         try {
396             if (mSessionHandlerThread != null) {
397                 mSessionHandlerThread.quitSafely();
398                 mSessionHandlerThread.join();
399             }
400         } catch (Exception e) {
401             Log.e(TAG, "Error while closing session thread.", e);
402         }
403         super.onDestroy();
404     }
405 }
406