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