1 /* 2 * Copyright 2023 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 androidx.camera.integration.core; 18 19 import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA; 20 import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore; 21 import static androidx.camera.testing.impl.FileUtil.createFolder; 22 import static androidx.camera.testing.impl.FileUtil.createParentFolder; 23 import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions; 24 import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions; 25 import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri; 26 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED; 27 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED; 28 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE; 29 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE; 30 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE; 31 32 import static com.google.common.base.Preconditions.checkNotNull; 33 34 import android.app.NotificationChannel; 35 import android.app.NotificationManager; 36 import android.app.PendingIntent; 37 import android.content.ContentValues; 38 import android.content.Intent; 39 import android.media.MediaScannerConnection; 40 import android.net.Uri; 41 import android.os.Binder; 42 import android.os.Build; 43 import android.os.Environment; 44 import android.os.IBinder; 45 import android.os.SystemClock; 46 import android.provider.MediaStore; 47 import android.util.Log; 48 import android.view.View; 49 import android.widget.RemoteViews; 50 import android.widget.Toast; 51 52 import androidx.annotation.RequiresApi; 53 import androidx.annotation.VisibleForTesting; 54 import androidx.camera.core.ImageAnalysis; 55 import androidx.camera.core.ImageCapture; 56 import androidx.camera.core.ImageCaptureException; 57 import androidx.camera.core.UseCase; 58 import androidx.camera.core.UseCaseGroup; 59 import androidx.camera.lifecycle.ProcessCameraProvider; 60 import androidx.camera.video.FileOutputOptions; 61 import androidx.camera.video.MediaStoreOutputOptions; 62 import androidx.camera.video.OutputOptions; 63 import androidx.camera.video.PendingRecording; 64 import androidx.camera.video.Recorder; 65 import androidx.camera.video.Recording; 66 import androidx.camera.video.VideoCapture; 67 import androidx.camera.video.VideoRecordEvent; 68 import androidx.core.app.NotificationCompat; 69 import androidx.core.content.ContextCompat; 70 import androidx.core.util.Consumer; 71 import androidx.lifecycle.LifecycleService; 72 73 import com.google.common.util.concurrent.ListenableFuture; 74 75 import org.jspecify.annotations.NonNull; 76 import org.jspecify.annotations.Nullable; 77 78 import java.io.File; 79 import java.text.Format; 80 import java.text.SimpleDateFormat; 81 import java.util.ArrayList; 82 import java.util.Calendar; 83 import java.util.Collection; 84 import java.util.Collections; 85 import java.util.HashMap; 86 import java.util.HashSet; 87 import java.util.List; 88 import java.util.Locale; 89 import java.util.Map; 90 import java.util.Set; 91 import java.util.concurrent.CountDownLatch; 92 import java.util.concurrent.ExecutionException; 93 import java.util.concurrent.atomic.AtomicInteger; 94 95 /** 96 * A service used to test background UseCases binding and camera operations. 97 */ 98 public class CameraXService extends LifecycleService { 99 private static final String TAG = "CameraXService"; 100 private static final int NOTIFICATION_ID = 1; 101 private static final String CHANNEL_ID_SERVICE_INFO = "channel_service_info"; 102 private static final int FRAME_COUNT_TO_UPDATE_ANALYSIS_INFO = 60; 103 104 // Actions 105 public static final String ACTION_BIND_USE_CASES = 106 "androidx.camera.integration.core.intent.action.BIND_USE_CASES"; 107 public static final String ACTION_TAKE_PICTURE = 108 "androidx.camera.integration.core.intent.action.TAKE_PICTURE"; 109 public static final String ACTION_START_RECORDING = 110 "androidx.camera.integration.core.intent.action.START_RECORDING"; 111 public static final String ACTION_STOP_RECORDING = 112 "androidx.camera.integration.core.intent.action.STOP_RECORDING"; 113 public static final String ACTION_STOP_SERVICE = 114 "androidx.camera.integration.core.intent.action.STOP_SERVICE"; 115 116 // Extras 117 public static final String EXTRA_VIDEO_CAPTURE_ENABLED = "EXTRA_VIDEO_CAPTURE_ENABLED"; 118 public static final String EXTRA_IMAGE_CAPTURE_ENABLED = "EXTRA_IMAGE_CAPTURE_ENABLED"; 119 public static final String EXTRA_IMAGE_ANALYSIS_ENABLED = "EXTRA_IMAGE_ANALYSIS_ENABLED"; 120 121 private final IBinder mBinder = new CameraXServiceBinder(); 122 private final AtomicInteger mAnalysisFrameCount = new AtomicInteger(0); 123 124 //////////////////////////////////////////////////////////////////////////////////////////////// 125 // Members only accessed on main thread // 126 //////////////////////////////////////////////////////////////////////////////////////////////// 127 private final Map<Class<?>, UseCase> mBoundUseCases = new HashMap<>(); 128 private @Nullable Recording mActiveRecording; 129 private NotificationCompat.@Nullable Builder mNotificationBuilder; 130 //--------------------------------------------------------------------------------------------// 131 132 //////////////////////////////////////////////////////////////////////////////////////////////// 133 // Members for testing // 134 //////////////////////////////////////////////////////////////////////////////////////////////// 135 private final Set<Uri> mSavedMediaUri = new HashSet<>(); 136 137 private @Nullable Consumer<Collection<UseCase>> mOnUseCaseBoundCallback; 138 private @Nullable CountDownLatch mAnalysisFrameLatch; 139 private @Nullable CountDownLatch mTakePictureLatch; 140 private @Nullable CountDownLatch mRecordVideoLatch; 141 //--------------------------------------------------------------------------------------------// 142 143 @Override onCreate()144 public void onCreate() { 145 super.onCreate(); 146 makeForeground(); 147 } 148 149 @Override onBind(@onNull Intent intent)150 public @Nullable IBinder onBind(@NonNull Intent intent) { 151 super.onBind(intent); 152 return mBinder; 153 } 154 155 @Override onStartCommand(@ullable Intent intent, int flags, int startId)156 public int onStartCommand(@Nullable Intent intent, int flags, int startId) { 157 if (intent != null) { 158 String action = intent.getAction(); 159 Log.d(TAG, "onStartCommand: action = " + action + ", extras = " + intent.getExtras()); 160 if (ACTION_BIND_USE_CASES.equals(action)) { 161 bindToLifecycle(intent); 162 } else if (ACTION_TAKE_PICTURE.equals(action)) { 163 takePicture(); 164 } else if (ACTION_START_RECORDING.equals(action)) { 165 startRecording(); 166 } else if (ACTION_STOP_RECORDING.equals(action)) { 167 stopRecording(); 168 } else if (ACTION_STOP_SERVICE.equals(action)) { 169 stopForeground(true); 170 stopSelf(); 171 } 172 } 173 return super.onStartCommand(intent, flags, startId); 174 } 175 makeForeground()176 private void makeForeground() { 177 createNotificationChannel(); 178 mNotificationBuilder = new NotificationCompat.Builder(this, 179 CHANNEL_ID_SERVICE_INFO) 180 .setSmallIcon(android.R.drawable.ic_menu_camera) 181 .setStyle(new NotificationCompat.DecoratedCustomViewStyle()) 182 .setCustomContentView(getNotificationView()); 183 startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); 184 } 185 bindToLifecycle(@onNull Intent intent)186 private void bindToLifecycle(@NonNull Intent intent) { 187 ListenableFuture<ProcessCameraProvider> cameraProviderFuture = 188 ProcessCameraProvider.getInstance(this); 189 ProcessCameraProvider cameraProvider; 190 try { 191 cameraProvider = cameraProviderFuture.get(); 192 } catch (ExecutionException | InterruptedException e) { 193 throw new IllegalStateException(e); 194 } 195 cameraProvider.unbindAll(); 196 mBoundUseCases.clear(); 197 UseCaseGroup useCaseGroup = resolveUseCaseGroup(intent); 198 List<UseCase> boundUseCases = Collections.emptyList(); 199 if (useCaseGroup != null) { 200 try { 201 cameraProvider.bindToLifecycle(this, DEFAULT_BACK_CAMERA, useCaseGroup); 202 boundUseCases = useCaseGroup.getUseCases(); 203 } catch (IllegalArgumentException e) { 204 String msg = "Failed to bind by " + e; 205 Log.w(TAG, msg, e); 206 Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); 207 } 208 } 209 onUseCaseBound(boundUseCases); 210 } 211 resolveUseCaseGroup(@onNull Intent intent)212 private @Nullable UseCaseGroup resolveUseCaseGroup(@NonNull Intent intent) { 213 boolean hasUseCase = false; 214 UseCaseGroup.Builder useCaseGroupBuilder = new UseCaseGroup.Builder(); 215 216 if (intent.getBooleanExtra(EXTRA_VIDEO_CAPTURE_ENABLED, false)) { 217 Recorder recorder = new Recorder.Builder().build(); 218 VideoCapture<?> videoCapture = new VideoCapture.Builder<>(recorder).build(); 219 useCaseGroupBuilder.addUseCase(videoCapture); 220 hasUseCase = true; 221 } 222 if (intent.getBooleanExtra(EXTRA_IMAGE_CAPTURE_ENABLED, false)) { 223 ImageCapture imageCapture = new ImageCapture.Builder().build(); 224 useCaseGroupBuilder.addUseCase(imageCapture); 225 hasUseCase = true; 226 } 227 if (intent.getBooleanExtra(EXTRA_IMAGE_ANALYSIS_ENABLED, false)) { 228 ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build(); 229 imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), mAnalyzer); 230 useCaseGroupBuilder.addUseCase(imageAnalysis); 231 hasUseCase = true; 232 } 233 234 return hasUseCase ? useCaseGroupBuilder.build() : null; 235 } 236 onUseCaseBound(@onNull List<UseCase> boundUseCases)237 private void onUseCaseBound(@NonNull List<UseCase> boundUseCases) { 238 Log.d(TAG, "Bound UseCases: " + boundUseCases); 239 for (UseCase boundUseCase : boundUseCases) { 240 mBoundUseCases.put(boundUseCase.getClass(), boundUseCase); 241 } 242 if (mOnUseCaseBoundCallback != null) { 243 mOnUseCaseBoundCallback.accept(boundUseCases); 244 } 245 mAnalysisFrameCount.set(0); 246 updateNotification(); 247 Toast.makeText(CameraXService.this, getHumanReadableName(boundUseCases) + " is bound", 248 Toast.LENGTH_SHORT).show(); 249 } 250 createNotificationChannel()251 private void createNotificationChannel() { 252 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 253 NotificationChannel serviceChannel = Api26Impl.newNotificationChannel( 254 CHANNEL_ID_SERVICE_INFO, 255 getString(R.string.camerax_service), 256 NotificationManager.IMPORTANCE_DEFAULT 257 ); 258 Api26Impl.createNotificationChannel(getNotificationManager(), serviceChannel); 259 } 260 } 261 getNotificationManager()262 private @NonNull NotificationManager getNotificationManager() { 263 return checkNotNull(ContextCompat.getSystemService(this, NotificationManager.class)); 264 } 265 updateNotification()266 private void updateNotification() { 267 NotificationCompat.Builder builder = checkNotNull(mNotificationBuilder); 268 builder.setCustomContentView(getNotificationView()); 269 getNotificationManager().notify(NOTIFICATION_ID, builder.build()); 270 } 271 getNotificationView()272 private @NonNull RemoteViews getNotificationView() { 273 RemoteViews notificationView = new RemoteViews(getPackageName(), 274 R.layout.notification_service_collapsed); 275 276 // Update VideoCapture view 277 if (getVideoCapture() != null) { 278 PendingIntent recordingIntent; 279 int recordingIconResId; 280 int recordingStateVisibility; 281 RemoteViews videoView = new RemoteViews(getPackageName(), 282 R.layout.notification_video_widget); 283 if (mActiveRecording == null) { 284 int flags = PendingIntent.FLAG_UPDATE_CURRENT; 285 if (Build.VERSION.SDK_INT >= 23) { 286 flags |= PendingIntent.FLAG_IMMUTABLE; 287 } 288 recordingIntent = PendingIntent.getService(this, 0, 289 new Intent(ACTION_START_RECORDING), flags); 290 recordingIconResId = android.R.drawable.ic_media_play; 291 recordingStateVisibility = View.GONE; 292 } else { 293 recordingIntent = PendingIntent.getService(this, 0, 294 new Intent(ACTION_STOP_RECORDING), 295 PendingIntent.FLAG_IMMUTABLE); 296 recordingIconResId = R.drawable.ic_media_stop; 297 recordingStateVisibility = View.VISIBLE; 298 } 299 videoView.setOnClickPendingIntent(R.id.control, recordingIntent); 300 videoView.setImageViewResource(R.id.control, recordingIconResId); 301 videoView.setViewVisibility(R.id.state, recordingStateVisibility); 302 notificationView.addView(R.id.video_container, videoView); 303 } 304 305 // Update ImageCapture view 306 if (getImageCapture() != null) { 307 RemoteViews imageView = new RemoteViews(getPackageName(), 308 R.layout.notification_image_widget); 309 PendingIntent takePictureIntent = PendingIntent.getService(this, 0, 310 new Intent(ACTION_TAKE_PICTURE), 311 PendingIntent.FLAG_IMMUTABLE); 312 imageView.setOnClickPendingIntent(R.id.picture, takePictureIntent); 313 notificationView.addView(R.id.image_container, imageView); 314 } 315 316 // Update ImageAnalysis view 317 if (getImageAnalysis() != null) { 318 RemoteViews analysisView = new RemoteViews(getPackageName(), 319 R.layout.notification_analysis_widget); 320 String analysisMsg = "Analysis:" + mAnalysisFrameCount.get(); 321 analysisView.setTextViewText(R.id.text_view, analysisMsg); 322 notificationView.addView(R.id.analysis_container, analysisView); 323 } 324 325 // Update exit button 326 notificationView.setOnClickPendingIntent(R.id.exit, 327 PendingIntent.getService(this, 0, new Intent(ACTION_STOP_SERVICE), 328 PendingIntent.FLAG_IMMUTABLE)); 329 330 return notificationView; 331 } 332 getImageCapture()333 private @Nullable ImageCapture getImageCapture() { 334 return (ImageCapture) mBoundUseCases.get(ImageCapture.class); 335 } 336 337 @SuppressWarnings("unchecked") getVideoCapture()338 private @Nullable VideoCapture<Recorder> getVideoCapture() { 339 return (VideoCapture<Recorder>) mBoundUseCases.get(VideoCapture.class); 340 } 341 getImageAnalysis()342 private @Nullable ImageAnalysis getImageAnalysis() { 343 return (ImageAnalysis) mBoundUseCases.get(ImageAnalysis.class); 344 } 345 takePicture()346 private void takePicture() { 347 ImageCapture imageCapture = getImageCapture(); 348 if (imageCapture == null) { 349 Log.w(TAG, "ImageCapture is not bound."); 350 return; 351 } 352 createDefaultPictureFolderIfNotExist(); 353 Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US); 354 String fileName = "ServiceTestApp-" + formatter.format(Calendar.getInstance().getTime()) 355 + ".jpg"; 356 ContentValues contentValues = new ContentValues(); 357 contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); 358 contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg"); 359 ImageCapture.OutputFileOptions outputFileOptions = 360 new ImageCapture.OutputFileOptions.Builder( 361 getContentResolver(), 362 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 363 contentValues).build(); 364 long startTimeMs = SystemClock.elapsedRealtime(); 365 imageCapture.takePicture(outputFileOptions, 366 ContextCompat.getMainExecutor(this), 367 new ImageCapture.OnImageSavedCallback() { 368 @Override 369 public void onImageSaved( 370 ImageCapture.@NonNull OutputFileResults outputFileResults) { 371 long durationMs = SystemClock.elapsedRealtime() - startTimeMs; 372 String msg = "Saved image " + outputFileResults.getSavedUri() 373 + " (" + durationMs + " ms)"; 374 Log.d(TAG, msg); 375 Toast.makeText(CameraXService.this, msg, Toast.LENGTH_LONG).show(); 376 mSavedMediaUri.add(outputFileResults.getSavedUri()); 377 if (mTakePictureLatch != null) { 378 mTakePictureLatch.countDown(); 379 } 380 } 381 382 @Override 383 public void onError(@NonNull ImageCaptureException exception) { 384 String msg = "Failed to save image by " + exception.getImageCaptureError(); 385 Log.e(TAG, msg, exception); 386 Toast.makeText(CameraXService.this, msg, Toast.LENGTH_SHORT).show(); 387 } 388 }); 389 } 390 createDefaultPictureFolderIfNotExist()391 private void createDefaultPictureFolderIfNotExist() { 392 File pictureFolder = Environment.getExternalStoragePublicDirectory( 393 Environment.DIRECTORY_PICTURES); 394 if (!createFolder(pictureFolder)) { 395 Log.e(TAG, "Failed to create directory: " + pictureFolder); 396 } 397 } 398 startRecording()399 private void startRecording() { 400 VideoCapture<Recorder> videoCapture = getVideoCapture(); 401 if (videoCapture == null) { 402 Log.w(TAG, "VideoCapture is not bound."); 403 return; 404 } 405 406 createDefaultVideoFolderIfNotExist(); 407 if (mActiveRecording == null) { 408 PendingRecording pendingRecording; 409 String fileName = "video_" + System.currentTimeMillis(); 410 String extension = "mp4"; 411 if (canDeviceWriteToMediaStore()) { 412 // Use MediaStoreOutputOptions for public share media storage. 413 pendingRecording = getVideoCapture().getOutput().prepareRecording( 414 this, 415 generateVideoMediaStoreOptions(getContentResolver(), fileName)); 416 } else { 417 // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk. 418 pendingRecording = getVideoCapture().getOutput().prepareRecording( 419 this, generateVideoFileOutputOptions(fileName, extension)); 420 } 421 //noinspection MissingPermission 422 mActiveRecording = pendingRecording 423 .withAudioEnabled() 424 .start(ContextCompat.getMainExecutor(this), mRecordingListener); 425 updateNotification(); 426 } else { 427 Log.e(TAG, "It should stop the active recording before start a new one."); 428 } 429 } 430 stopRecording()431 private void stopRecording() { 432 if (mActiveRecording != null) { 433 mActiveRecording.stop(); 434 mActiveRecording = null; 435 } 436 } 437 createDefaultVideoFolderIfNotExist()438 private void createDefaultVideoFolderIfNotExist() { 439 String videoFilePath = getAbsolutePathFromUri(getContentResolver(), 440 MediaStore.Video.Media.EXTERNAL_CONTENT_URI); 441 if (videoFilePath == null || !createParentFolder(videoFilePath)) { 442 Log.e(TAG, "Failed to create parent directory for: " + videoFilePath); 443 } 444 } 445 getHumanReadableName(@onNull List<UseCase> useCases)446 private static @NonNull String getHumanReadableName(@NonNull List<UseCase> useCases) { 447 List<String> useCaseNames = new ArrayList<>(); 448 for (UseCase useCase : useCases) { 449 useCaseNames.add(useCase.getClass().getSimpleName()); 450 } 451 return useCaseNames.size() > 0 ? String.join(" | ", useCaseNames) : "No UseCase"; 452 } 453 454 private final ImageAnalysis.Analyzer mAnalyzer = image -> { 455 if (mAnalysisFrameCount.getAndIncrement() % FRAME_COUNT_TO_UPDATE_ANALYSIS_INFO == 0) { 456 updateNotification(); 457 } 458 if (mAnalysisFrameLatch != null) { 459 mAnalysisFrameLatch.countDown(); 460 } 461 image.close(); 462 }; 463 464 private final Consumer<VideoRecordEvent> mRecordingListener = event -> { 465 if (event instanceof VideoRecordEvent.Finalize) { 466 VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) event; 467 468 switch (finalize.getError()) { 469 case ERROR_NONE: 470 case ERROR_FILE_SIZE_LIMIT_REACHED: 471 case ERROR_DURATION_LIMIT_REACHED: 472 case ERROR_INSUFFICIENT_STORAGE: 473 case ERROR_SOURCE_INACTIVE: 474 Uri uri = finalize.getOutputResults().getOutputUri(); 475 OutputOptions outputOptions = finalize.getOutputOptions(); 476 String msg; 477 String videoFilePath; 478 if (outputOptions instanceof MediaStoreOutputOptions) { 479 msg = "Saved video " + uri; 480 videoFilePath = getAbsolutePathFromUri( 481 getApplicationContext().getContentResolver(), 482 uri 483 ); 484 } else if (outputOptions instanceof FileOutputOptions) { 485 videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath(); 486 MediaScannerConnection.scanFile(this, 487 new String[]{videoFilePath}, null, 488 (path, uri1) -> Log.i(TAG, "Scanned " + path + " -> uri= " + uri1)); 489 msg = "Saved video " + videoFilePath; 490 } else { 491 throw new AssertionError("Unknown or unsupported OutputOptions type: " 492 + outputOptions.getClass().getSimpleName()); 493 } 494 // The video file path is used in tracing e2e test log. Don't remove it. 495 Log.d(TAG, "Saved video file: " + videoFilePath); 496 497 if (finalize.getError() != ERROR_NONE) { 498 msg += " with code (" + finalize.getError() + ")"; 499 } 500 Log.d(TAG, msg, finalize.getCause()); 501 Toast.makeText(CameraXService.this, msg, Toast.LENGTH_LONG).show(); 502 503 mSavedMediaUri.add(uri); 504 if (mRecordVideoLatch != null) { 505 mRecordVideoLatch.countDown(); 506 } 507 break; 508 default: 509 String errMsg = "Video capture failed by (" + finalize.getError() + "): " 510 + finalize.getCause(); 511 Log.e(TAG, errMsg, finalize.getCause()); 512 Toast.makeText(CameraXService.this, errMsg, Toast.LENGTH_LONG).show(); 513 } 514 mActiveRecording = null; 515 updateNotification(); 516 } 517 }; 518 519 @RequiresApi(26) 520 static class Api26Impl { 521 Api26Impl()522 private Api26Impl() { 523 } 524 525 /** @noinspection SameParameterValue */ newNotificationChannel(@onNull String id, @NonNull CharSequence name, int importance)526 static @NonNull NotificationChannel newNotificationChannel(@NonNull String id, 527 @NonNull CharSequence name, int importance) { 528 return new NotificationChannel(id, name, importance); 529 } 530 createNotificationChannel(@onNull NotificationManager manager, @NonNull NotificationChannel channel)531 static void createNotificationChannel(@NonNull NotificationManager manager, 532 @NonNull NotificationChannel channel) { 533 manager.createNotificationChannel(channel); 534 } 535 } 536 537 @VisibleForTesting setOnUseCaseBoundCallback(@onNull Consumer<Collection<UseCase>> callback)538 void setOnUseCaseBoundCallback(@NonNull Consumer<Collection<UseCase>> callback) { 539 mOnUseCaseBoundCallback = callback; 540 } 541 542 @VisibleForTesting acquireAnalysisFrameCountDownLatch()543 @NonNull CountDownLatch acquireAnalysisFrameCountDownLatch() { 544 mAnalysisFrameLatch = new CountDownLatch(3); 545 return mAnalysisFrameLatch; 546 } 547 548 @VisibleForTesting acquireTakePictureCountDownLatch()549 @NonNull CountDownLatch acquireTakePictureCountDownLatch() { 550 mTakePictureLatch = new CountDownLatch(1); 551 return mTakePictureLatch; 552 } 553 554 @VisibleForTesting acquireRecordVideoCountDownLatch()555 @NonNull CountDownLatch acquireRecordVideoCountDownLatch() { 556 mRecordVideoLatch = new CountDownLatch(1); 557 return mRecordVideoLatch; 558 } 559 560 @VisibleForTesting deleteSavedMediaFiles()561 void deleteSavedMediaFiles() { 562 deleteUriSet(mSavedMediaUri); 563 } 564 deleteUriSet(@onNull Set<Uri> uriSet)565 private void deleteUriSet(@NonNull Set<Uri> uriSet) { 566 for (Uri uri : uriSet) { 567 try { 568 getContentResolver().delete(uri, null, null); 569 } catch (RuntimeException e) { 570 Log.w(TAG, "Unable to delete uri: " + uri, e); 571 } 572 } 573 } 574 575 class CameraXServiceBinder extends Binder { getService()576 @NonNull CameraXService getService() { 577 return CameraXService.this; 578 } 579 } 580 } 581