Add more core APIs to CameraController.

Users have been requesting to add more core APIs to CameraController. After another round of internal discussion(go/camera-controller-do-not-inherit), we have decided CameraController should continue to be a wrapper that forwards API calls. This CL adds some of the core features to CameraController.

- Getters/setters for target aspect ratio and target resolution. A new OutputSize class that defines the aspect ratio or resolution.
- Getter for CameraControl
- Getters/setters for capture mode.
- Getters/fetters for custom executors.
- Unlink VideoCapture in javadoc because it's hidden now.

Relnote: adding more camera-core features to CameraController: getters/setters for target aspect ratio, target resolution, capture mode, CameraControl and custom executors.

Bug: 188543457
Test: manual test and ./gradlew bOS
Change-Id: Iea8f2b507fa9783e6b63d42ee5f47ec867c5151a
diff --git a/camera/camera-view/api/current.txt b/camera/camera-view/api/current.txt
index d9c61fc..a56d9d4 100644
--- a/camera/camera-view/api/current.txt
+++ b/camera/camera-view/api/current.txt
@@ -4,13 +4,21 @@
   public abstract class CameraController {
     method @MainThread public void clearImageAnalysisAnalyzer();
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+    method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
     method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
     method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+    method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
     method @MainThread public int getImageAnalysisBackpressureStrategy();
     method @MainThread public int getImageAnalysisImageQueueDepth();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
     method @MainThread public int getImageCaptureFlashMode();
+    method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+    method @MainThread public int getImageCaptureMode();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
     method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
     method @MainThread public boolean isImageAnalysisEnabled();
@@ -20,12 +28,19 @@
     method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
     method @MainThread public void setEnabledUseCases(int);
     method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+    method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
     method @MainThread public void setImageAnalysisBackpressureStrategy(int);
     method @MainThread public void setImageAnalysisImageQueueDepth(int);
+    method @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setImageCaptureFlashMode(int);
+    method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+    method @MainThread public void setImageCaptureMode(int);
+    method @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
     method @MainThread public void setPinchToZoomEnabled(boolean);
+    method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setTapToFocusEnabled(boolean);
+    method @MainThread public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
@@ -33,6 +48,14 @@
     field public static final int IMAGE_CAPTURE = 1; // 0x1
   }
 
+  public static class CameraController.OutputSize {
+    ctor public CameraController.OutputSize(int);
+    ctor public CameraController.OutputSize(android.util.Size);
+    method public int getAspectRatio();
+    method public android.util.Size? getResolution();
+    field public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+  }
+
   public final class LifecycleCameraController extends androidx.camera.view.CameraController {
     ctor public LifecycleCameraController(android.content.Context);
     method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
diff --git a/camera/camera-view/api/public_plus_experimental_current.txt b/camera/camera-view/api/public_plus_experimental_current.txt
index a02bc5e..798e132 100644
--- a/camera/camera-view/api/public_plus_experimental_current.txt
+++ b/camera/camera-view/api/public_plus_experimental_current.txt
@@ -4,13 +4,21 @@
   public abstract class CameraController {
     method @MainThread public void clearImageAnalysisAnalyzer();
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+    method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
     method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
     method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+    method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
     method @MainThread @androidx.camera.core.ImageAnalysis.BackpressureStrategy public int getImageAnalysisBackpressureStrategy();
     method @MainThread public int getImageAnalysisImageQueueDepth();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
     method @MainThread @androidx.camera.core.ImageCapture.FlashMode public int getImageCaptureFlashMode();
+    method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+    method @MainThread public int getImageCaptureMode();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
     method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
     method @MainThread public boolean isImageAnalysisEnabled();
@@ -22,12 +30,19 @@
     method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
     method @MainThread public void setEnabledUseCases(int);
     method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+    method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
     method @MainThread public void setImageAnalysisBackpressureStrategy(@androidx.camera.core.ImageAnalysis.BackpressureStrategy int);
     method @MainThread public void setImageAnalysisImageQueueDepth(int);
+    method @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setImageCaptureFlashMode(@androidx.camera.core.ImageCapture.FlashMode int);
+    method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+    method @MainThread public void setImageCaptureMode(@androidx.camera.core.ImageCapture.CaptureMode int);
+    method @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
     method @MainThread public void setPinchToZoomEnabled(boolean);
+    method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setTapToFocusEnabled(boolean);
+    method @MainThread public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method @MainThread @androidx.camera.view.video.ExperimentalVideo public void startRecording(androidx.camera.view.video.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.view.video.OnVideoSavedCallback);
     method @MainThread @androidx.camera.view.video.ExperimentalVideo public void stopRecording();
@@ -38,6 +53,14 @@
     field @androidx.camera.view.video.ExperimentalVideo public static final int VIDEO_CAPTURE = 4; // 0x4
   }
 
+  public static class CameraController.OutputSize {
+    ctor public CameraController.OutputSize(@androidx.camera.core.AspectRatio.Ratio int);
+    ctor public CameraController.OutputSize(android.util.Size);
+    method public int getAspectRatio();
+    method public android.util.Size? getResolution();
+    field public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+  }
+
   public final class LifecycleCameraController extends androidx.camera.view.CameraController {
     ctor public LifecycleCameraController(android.content.Context);
     method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
diff --git a/camera/camera-view/api/restricted_current.txt b/camera/camera-view/api/restricted_current.txt
index a47bbc3..c880826 100644
--- a/camera/camera-view/api/restricted_current.txt
+++ b/camera/camera-view/api/restricted_current.txt
@@ -4,13 +4,21 @@
   public abstract class CameraController {
     method @MainThread public void clearImageAnalysisAnalyzer();
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+    method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
     method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
     method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+    method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
     method @MainThread @androidx.camera.core.ImageAnalysis.BackpressureStrategy public int getImageAnalysisBackpressureStrategy();
     method @MainThread public int getImageAnalysisImageQueueDepth();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
     method @MainThread @androidx.camera.core.ImageCapture.FlashMode public int getImageCaptureFlashMode();
+    method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+    method @MainThread public int getImageCaptureMode();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+    method @MainThread public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
     method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
     method @MainThread public boolean isImageAnalysisEnabled();
@@ -20,12 +28,19 @@
     method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
     method @MainThread public void setEnabledUseCases(int);
     method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+    method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
     method @MainThread public void setImageAnalysisBackpressureStrategy(@androidx.camera.core.ImageAnalysis.BackpressureStrategy int);
     method @MainThread public void setImageAnalysisImageQueueDepth(int);
+    method @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setImageCaptureFlashMode(@androidx.camera.core.ImageCapture.FlashMode int);
+    method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+    method @MainThread public void setImageCaptureMode(@androidx.camera.core.ImageCapture.CaptureMode int);
+    method @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
     method @MainThread public void setPinchToZoomEnabled(boolean);
+    method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setTapToFocusEnabled(boolean);
+    method @MainThread public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
@@ -33,6 +48,14 @@
     field public static final int IMAGE_CAPTURE = 1; // 0x1
   }
 
+  public static class CameraController.OutputSize {
+    ctor public CameraController.OutputSize(@androidx.camera.core.AspectRatio.Ratio int);
+    ctor public CameraController.OutputSize(android.util.Size);
+    method public int getAspectRatio();
+    method public android.util.Size? getResolution();
+    field public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+  }
+
   public final class LifecycleCameraController extends androidx.camera.view.CameraController {
     ctor public LifecycleCameraController(android.content.Context);
     method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 98d607f..2fe7a79 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -16,12 +16,15 @@
 
 package androidx.camera.view;
 
+import static androidx.camera.view.CameraController.OutputSize.UNASSIGNED_ASPECT_RATIO;
+
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.hardware.display.DisplayManager;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
+import android.util.Size;
 import android.view.Display;
 
 import androidx.annotation.DoNotInline;
@@ -34,6 +37,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.AspectRatio;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraInfo;
@@ -56,6 +60,7 @@
 import androidx.camera.core.VideoCapture;
 import androidx.camera.core.ViewPort;
 import androidx.camera.core.ZoomState;
+import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.Futures;
@@ -150,32 +155,50 @@
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
     @NonNull
-    final Preview mPreview;
+    Preview mPreview;
+
+    @Nullable
+    OutputSize mPreviewTargetSize;
 
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
     @NonNull
-    final ImageCapture mImageCapture;
+    ImageCapture mImageCapture;
+
+    @Nullable
+    OutputSize mImageCaptureTargetSize;
+
+    @Nullable
+    Executor mImageCaptureIoExecutor;
 
     @Nullable
     private Executor mAnalysisExecutor;
 
     @Nullable
+    private Executor mAnalysisBackgroundExecutor;
+
+    @Nullable
     private ImageAnalysis.Analyzer mAnalysisAnalyzer;
 
     @NonNull
     ImageAnalysis mImageAnalysis;
 
+    @Nullable
+    OutputSize mImageAnalysisTargetSize;
+
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
     @NonNull
-    final VideoCapture mVideoCapture;
+    VideoCapture mVideoCapture;
 
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
     @NonNull
     final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
 
+    @Nullable
+    OutputSize mVideoCaptureOutputSize;
+
     // The latest bound camera.
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
@@ -375,6 +398,35 @@
         return (mEnabledUseCases & useCaseMask) != 0;
     }
 
+    /**
+     * Sets the target aspect ratio or target resolution based on {@link OutputSize}.
+     */
+    private void setTargetOutputSize(@NonNull ImageOutputConfig.Builder<?> builder,
+            @Nullable OutputSize outputSize) {
+        if (outputSize == null) {
+            return;
+        }
+        if (outputSize.getResolution() != null) {
+            builder.setTargetResolution(outputSize.getResolution());
+        } else if (outputSize.getAspectRatio() != UNASSIGNED_ASPECT_RATIO) {
+            builder.setTargetAspectRatio(outputSize.getAspectRatio());
+        } else {
+            Logger.e(TAG, "Invalid target surface size. " + outputSize);
+        }
+    }
+
+    /**
+     * Checks if two {@link OutputSize} are equal.
+     */
+    private boolean isOutputSizeEqual(
+            @Nullable OutputSize currentSize,
+            @Nullable OutputSize newSize) {
+        if (currentSize == newSize) {
+            return true;
+        }
+        return currentSize != null && currentSize.equals(newSize);
+    }
+
     // ------------------
     // Preview use case.
     // ------------------
@@ -433,6 +485,55 @@
         return (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE);
     }
 
+    /**
+     * Sets the intended output size for {@link Preview}.
+     *
+     * <p> The value is used as a hint when determining the resolution and aspect ratio of the
+     * preview. The actual output may differ from the requested value due to device constraints.
+     *
+     * <p> When set to null, the output will be based on the default config of {@link Preview}.
+     *
+     * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+     * To avoid this, set the target size before controller is bound to lifecycle.
+     *
+     * @param targetSize the intended output size for {@link Preview}.
+     * @see Preview.Builder#setTargetAspectRatio(int)
+     * @see Preview.Builder#setTargetResolution(Size)
+     */
+    @MainThread
+    public void setPreviewTargetSize(@Nullable OutputSize targetSize) {
+        Threads.checkMainThread();
+        if (isOutputSizeEqual(mPreviewTargetSize, targetSize)) {
+            return;
+        }
+        mPreviewTargetSize = targetSize;
+        unbindPreviewAndRecreate();
+        startCameraAndTrackStates();
+    }
+
+    /**
+     * Returns the intended output size for {@link Preview} set by
+     * {@link #setPreviewTargetSize(OutputSize)}, or null if not set.
+     */
+    @MainThread
+    @Nullable
+    public OutputSize getPreviewTargetSize() {
+        Threads.checkMainThread();
+        return mPreviewTargetSize;
+    }
+
+    /**
+     * Unbinds {@link Preview} and recreates with the latest parameters.
+     */
+    private void unbindPreviewAndRecreate() {
+        if (isCameraInitialized()) {
+            mCameraProvider.unbind(mPreview);
+        }
+        Preview.Builder builder = new Preview.Builder();
+        setTargetOutputSize(builder, mPreviewTargetSize);
+        mPreview = builder.build();
+    }
+
     // ----------------------
     // ImageCapture UseCase.
     // ----------------------
@@ -546,6 +647,120 @@
         mImageCapture.takePicture(executor, callback);
     }
 
+    /**
+     * Sets the image capture mode.
+     *
+     * <p>Valid capture modes are {@link ImageCapture.CaptureMode#CAPTURE_MODE_MINIMIZE_LATENCY},
+     * which prioritizes latency over image quality, or
+     * {@link ImageCapture.CaptureMode#CAPTURE_MODE_MAXIMIZE_QUALITY},
+     * which prioritizes image quality over latency.
+     *
+     * @param captureMode the requested image capture mode.
+     */
+    @MainThread
+    public void setImageCaptureMode(@ImageCapture.CaptureMode int captureMode) {
+        Threads.checkMainThread();
+        if (mImageCapture.getCaptureMode() == captureMode) {
+            return;
+        }
+        unbindImageCaptureAndRecreate(captureMode);
+        startCameraAndTrackStates();
+    }
+
+    /**
+     * Returns the image capture mode.
+     *
+     * @see ImageCapture#getCaptureMode()
+     */
+    @MainThread
+    public int getImageCaptureMode() {
+        Threads.checkMainThread();
+        return mImageCapture.getCaptureMode();
+    }
+
+    /**
+     * Sets the intended image size for {@link ImageCapture}.
+     *
+     * <p> The value is used as a hint when determining the resolution and aspect ratio of
+     * the captured image. The actual output may differ from the requested value due to device
+     * constraints.
+     *
+     * <p> When set to null, the output will be based on the default config of {@link ImageCapture}.
+     *
+     * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+     * To avoid this, set the target size before controller is bound to lifecycle.
+     *
+     * @param targetSize the intended image size for {@link ImageCapture}.
+     */
+    @MainThread
+    public void setImageCaptureTargetSize(@Nullable OutputSize targetSize) {
+        Threads.checkMainThread();
+        if (isOutputSizeEqual(mImageCaptureTargetSize, targetSize)) {
+            return;
+        }
+        mImageCaptureTargetSize = targetSize;
+        unbindImageCaptureAndRecreate(getImageCaptureMode());
+        startCameraAndTrackStates();
+    }
+
+    /**
+     * Returns the intended output size for {@link ImageCapture} set by
+     * {@link #setImageCaptureTargetSize(OutputSize)}, or null if not set.
+     */
+    @MainThread
+    @Nullable
+    public OutputSize getImageCaptureTargetSize() {
+        Threads.checkMainThread();
+        return mImageCaptureTargetSize;
+    }
+
+    /**
+     * Sets the default executor that will be used for {@link ImageCapture} IO tasks.
+     *
+     * <p> This executor will be used for any IO tasks specifically for {@link ImageCapture},
+     * such as {@link #takePicture(ImageCapture.OutputFileOptions, Executor,
+     * ImageCapture.OnImageSavedCallback)}. If no executor is set, then a default Executor
+     * specifically for IO will be used instead.
+     *
+     * @param executor The executor which will be used for IO tasks.
+     *                 TODO(b/187842789) add @see link for ImageCapture.
+     */
+    @MainThread
+    public void setImageCaptureIoExecutor(@Nullable Executor executor) {
+        Threads.checkMainThread();
+        if (mImageCaptureIoExecutor == executor) {
+            return;
+        }
+        mImageCaptureIoExecutor = executor;
+        unbindImageCaptureAndRecreate(mImageCapture.getCaptureMode());
+        startCameraAndTrackStates();
+    }
+
+    /**
+     * Gets the default executor for {@link ImageCapture} IO tasks.
+     */
+    @MainThread
+    @Nullable
+    public Executor getImageCaptureIoExecutor() {
+        Threads.checkMainThread();
+        return mImageCaptureIoExecutor;
+    }
+
+    /**
+     * Unbinds {@link ImageCapture} and recreates with the latest parameters.
+     */
+    private void unbindImageCaptureAndRecreate(int imageCaptureMode) {
+        if (isCameraInitialized()) {
+            mCameraProvider.unbind(mImageCapture);
+        }
+        ImageCapture.Builder builder = new ImageCapture.Builder().setCaptureMode(imageCaptureMode);
+        setTargetOutputSize(builder, mImageCaptureTargetSize);
+        if (mImageCaptureIoExecutor != null) {
+            builder.setIoExecutor(mImageCaptureIoExecutor);
+        }
+        mImageCapture = builder.build();
+    }
+
     // -----------------
     // Image analysis
     // -----------------
@@ -673,19 +888,94 @@
     }
 
     /**
-     * Unbinds {@link ImageAnalysis} and recreates with the given parameters.
+     * Sets the intended output size for {@link ImageAnalysis}.
      *
-     * <p> This is necessary because unlike other use cases, {@link ImageAnalysis}'s parameters
-     * cannot be updated without recreating the use case.
+     * <p> The value is used as a hint when determining the resolution and aspect ratio of
+     * the output buffer. The actual output may differ from the requested value due to device
+     * constraints.
+     *
+     * <p> When set to null, the output will be based on the default config of
+     * {@link ImageAnalysis}.
+     *
+     * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+     * To avoid this, set the target size before controller is bound to lifecycle.
+     *
+     * @param targetSize the intended output size for {@link ImageAnalysis}.
+     * @see ImageAnalysis.Builder#setTargetAspectRatio(int)
+     * @see ImageAnalysis.Builder#setTargetResolution(Size)
+     */
+    @MainThread
+    public void setImageAnalysisTargetSize(@Nullable OutputSize targetSize) {
+        Threads.checkMainThread();
+        if (isOutputSizeEqual(mImageAnalysisTargetSize, targetSize)) {
+            return;
+        }
+        mImageAnalysisTargetSize = targetSize;
+        unbindImageAnalysisAndRecreate(
+                mImageAnalysis.getBackpressureStrategy(),
+                mImageAnalysis.getImageQueueDepth());
+        startCameraAndTrackStates();
+    }
+
+    /**
+     * Returns the intended output size for {@link ImageAnalysis} set by
+     * {@link #setImageAnalysisTargetSize(OutputSize)}, or null if not set.
+     */
+    @MainThread
+    @Nullable
+    public OutputSize getImageAnalysisTargetSize() {
+        Threads.checkMainThread();
+        return mImageAnalysisTargetSize;
+    }
+
+    /**
+     * Sets the executor that will be used for {@link ImageAnalysis} background tasks.
+     *
+     * <p>If not set, the background executor will default to an automatically generated
+     * {@link Executor}.
+     *
+     * @param executor the executor for {@link ImageAnalysis} background tasks.
+     * @see ImageAnalysis.Builder#setBackgroundExecutor(Executor)
+     */
+    @MainThread
+    public void setImageAnalysisBackgroundExecutor(@Nullable Executor executor) {
+        Threads.checkMainThread();
+        if (mAnalysisBackgroundExecutor == executor) {
+            return;
+        }
+        mAnalysisBackgroundExecutor = executor;
+        unbindImageAnalysisAndRecreate(mImageAnalysis.getBackpressureStrategy(),
+                mImageAnalysis.getImageQueueDepth());
+        startCameraAndTrackStates();
+    }
+
+    /**
+     * Gets the default executor for {@link ImageAnalysis} background tasks.
+     *
+     * @see ImageAnalysis.Builder#setBackgroundExecutor(Executor)
+     */
+    @MainThread
+    @Nullable
+    public Executor getImageAnalysisBackgroundExecutor() {
+        Threads.checkMainThread();
+        return mAnalysisBackgroundExecutor;
+    }
+
+    /**
+     * Unbinds {@link ImageAnalysis} and recreates with the latest parameters.
      */
     private void unbindImageAnalysisAndRecreate(int strategy, int imageQueueDepth) {
         if (isCameraInitialized()) {
             mCameraProvider.unbind(mImageAnalysis);
         }
-        mImageAnalysis = new ImageAnalysis.Builder()
+        ImageAnalysis.Builder builder = new ImageAnalysis.Builder()
                 .setBackpressureStrategy(strategy)
-                .setImageQueueDepth(imageQueueDepth)
-                .build();
+                .setImageQueueDepth(imageQueueDepth);
+        setTargetOutputSize(builder, mImageAnalysisTargetSize);
+        if (mAnalysisBackgroundExecutor != null) {
+            builder.setBackgroundExecutor(mAnalysisBackgroundExecutor);
+        }
+        mImageAnalysis = builder.build();
         if (mAnalysisExecutor != null && mAnalysisAnalyzer != null) {
             mImageAnalysis.setAnalyzer(mAnalysisExecutor, mAnalysisAnalyzer);
         }
@@ -765,6 +1055,53 @@
         return mVideoIsRecording.get();
     }
 
+    /**
+     * Sets the intended video size for {@code VideoCapture}.
+     *
+     * <p> The value is used as a hint when determining the resolution and aspect ratio of
+     * the video. The actual output may differ from the requested value due to device constraints.
+     *
+     * <p> When set to null, the output will be based on the default config of {@code VideoCapture}.
+     *
+     * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+     * To avoid this, set the target size before controller is bound to lifecycle.
+     *
+     * @param targetSize the intended video size for {@code VideoCapture}.
+     */
+    @MainThread
+    public void setVideoCaptureTargetSize(@Nullable OutputSize targetSize) {
+        Threads.checkMainThread();
+        if (isOutputSizeEqual(mVideoCaptureOutputSize, targetSize)) {
+            return;
+        }
+        mVideoCaptureOutputSize = targetSize;
+        unbindVideoAndRecreate();
+        startCameraAndTrackStates();
+    }
+
+    /**
+     * Returns the intended output size for {@code VideoCapture} set by
+     * {@link #setVideoCaptureTargetSize(OutputSize)}, or null if not set.
+     */
+    @MainThread
+    @Nullable
+    public OutputSize getVideoCaptureTargetSize() {
+        Threads.checkMainThread();
+        return mVideoCaptureOutputSize;
+    }
+
+    /**
+     * Unbinds VideoCapture and recreate with the latest parameters.
+     */
+    private void unbindVideoAndRecreate() {
+        if (isCameraInitialized()) {
+            mCameraProvider.unbind(mVideoCapture);
+        }
+        VideoCapture.Builder builder = new VideoCapture.Builder();
+        setTargetOutputSize(builder, mVideoCaptureOutputSize);
+        mVideoCapture = builder.build();
+    }
+
     // -----------------
     // Camera control
     // -----------------
@@ -985,6 +1322,12 @@
     /**
      * Gets the {@link CameraInfo} of the currently attached camera.
      *
+     * <p> For info available directly through CameraController as well as {@link CameraInfo},
+     * it's recommended to use the ones with CameraController, e.g. {@link #getTorchState()} v.s.
+     * {@link CameraInfo#getTorchState()}. {@link CameraInfo} is a lower-layer API and may
+     * require more steps to achieve the same effect, and will not maintain values when switching
+     * between cameras.
+     *
      * @return the {@link CameraInfo} of the current camera. Returns null if camera is not ready.
      * @see Camera#getCameraInfo()
      */
@@ -996,6 +1339,25 @@
     }
 
     /**
+     * Gets the {@link CameraControl} of the currently attached camera.
+     *
+     * <p> For controls available directly through CameraController as well as
+     * {@link CameraControl}, it's recommended to use the ones with CameraController, e.g.
+     * {@link #setLinearZoom(float)} v.s. {@link CameraControl#setLinearZoom(float)}.
+     * CameraControl is a lower-layer API and may require more steps to achieve the same effect,
+     * and will not maintain control values when switching between cameras.
+     *
+     * @return the {@link CameraControl} of the current camera. Returns null if camera is not ready.
+     * @see Camera#getCameraControl()
+     */
+    @Nullable
+    @MainThread
+    public CameraControl getCameraControl() {
+        Threads.checkMainThread();
+        return mCamera == null ? null : mCamera.getCameraControl();
+    }
+
+    /**
      * Sets current zoom by ratio.
      *
      * <p>Valid zoom values range from {@link ZoomState#getMinZoomRatio()} to
@@ -1221,4 +1583,84 @@
             return context.getAttributionTag();
         }
     }
+
+    /**
+     * Represents the output size of a {@link UseCase}.
+     *
+     * <p> This class is a preferred output size to be used with {@link CameraController}. The
+     * preferred output size can be based on either resolution or aspect ratio, but not both.
+     *
+     * @see #setImageAnalysisTargetSize(OutputSize)
+     * @see #setPreviewTargetSize(OutputSize)
+     * @see #setImageCaptureTargetSize(OutputSize)
+     * @see #setVideoCaptureTargetSize(OutputSize)
+     */
+    public static class OutputSize {
+
+        /**
+         * A value that represents the aspect ratio is not assigned.
+         */
+        public static final int UNASSIGNED_ASPECT_RATIO = -1;
+
+        /**
+         * Possible value for {@link #getAspectRatio()}
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(value = {UNASSIGNED_ASPECT_RATIO, AspectRatio.RATIO_4_3, AspectRatio.RATIO_16_9})
+        public @interface OutputAspectRatio {
+        }
+
+        @OutputAspectRatio
+        private final int mAspectRatio;
+
+        @Nullable
+        private final Size mResolution;
+
+        /**
+         * Creates a {@link OutputSize} that is based on aspect ratio.
+         *
+         * @see Preview.Builder#setTargetAspectRatio(int)
+         * @see ImageAnalysis.Builder#setTargetAspectRatio(int)
+         */
+        public OutputSize(@AspectRatio.Ratio int aspectRatio) {
+            Preconditions.checkArgument(aspectRatio != UNASSIGNED_ASPECT_RATIO);
+            mAspectRatio = aspectRatio;
+            mResolution = null;
+        }
+
+        /**
+         * Creates a {@link OutputSize} that is based on resolution.
+         *
+         * @see Preview.Builder#setTargetResolution(Size)
+         * @see ImageAnalysis.Builder#setTargetResolution(Size)
+         */
+        public OutputSize(@NonNull Size resolution) {
+            Preconditions.checkNotNull(resolution);
+            mAspectRatio = UNASSIGNED_ASPECT_RATIO;
+            mResolution = resolution;
+        }
+
+        /**
+         * Gets the value of aspect ratio.
+         *
+         * @return {@link #UNASSIGNED_ASPECT_RATIO} if the size is not based on aspect ratio.
+         */
+        @OutputAspectRatio
+        public int getAspectRatio() {
+            return mAspectRatio;
+        }
+
+        /**
+         * Gets the value of resolution.
+         *
+         * @return null if the size is not based on resolution.
+         */
+        @Nullable
+        public Size getResolution() {
+            return mResolution;
+        }
+    }
 }
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
index fe3a1c0..ce2e610 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
@@ -18,10 +18,17 @@
 
 import android.content.Context
 import android.os.Build
+import android.util.Size
 import android.view.Surface
+import androidx.camera.core.AspectRatio
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraX
 import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.impl.ImageAnalysisConfig
+import androidx.camera.core.impl.ImageCaptureConfig
+import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ApplicationProvider
@@ -33,6 +40,7 @@
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import java.util.concurrent.Executors
 
 /**
  * Unit tests for [CameraController].
@@ -43,6 +51,11 @@
 public class CameraControllerTest {
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var controller: CameraController
+    private val targetSizeWithAspectRatio =
+        CameraController.OutputSize(AspectRatio.RATIO_16_9)
+    private val targetSizeWithResolution =
+        CameraController.OutputSize(Size(1080, 1960))
 
     @Before
     public fun setUp() {
@@ -50,6 +63,7 @@
             FakeAppConfig.create()
         ).build()
         CameraX.initialize(context, cameraXConfig).get()
+        controller = LifecycleCameraController(context)
     }
 
     @After
@@ -59,24 +73,139 @@
 
     @UiThreadTest
     @Test
-    public fun sensorRotationChanges_useCaseTargetRotationUpdated() {
-        // Arrange.
-        val controller = LifecycleCameraController(context)
+    public fun setPreviewAspectRatio() {
+        controller.previewTargetSize = targetSizeWithAspectRatio
+        assertThat(controller.previewTargetSize).isEqualTo(targetSizeWithAspectRatio)
 
+        val config = controller.mPreview.currentConfig as ImageOutputConfig
+        assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setPreviewResolution() {
+        controller.previewTargetSize = targetSizeWithResolution
+        assertThat(controller.previewTargetSize).isEqualTo(targetSizeWithResolution)
+
+        val config = controller.mPreview.currentConfig as ImageOutputConfig
+        assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setAnalysisAspectRatio() {
+        controller.imageAnalysisTargetSize = targetSizeWithAspectRatio
+        assertThat(controller.imageAnalysisTargetSize).isEqualTo(targetSizeWithAspectRatio)
+
+        val config = controller.mImageAnalysis.currentConfig as ImageOutputConfig
+        assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setAnalysisBackgroundExecutor() {
+        val executor = Executors.newSingleThreadExecutor()
+        controller.imageAnalysisBackgroundExecutor = executor
+        assertThat(controller.imageAnalysisBackgroundExecutor).isEqualTo(executor)
+        val config = controller.mImageAnalysis.currentConfig as ImageAnalysisConfig
+        assertThat(config.backgroundExecutor).isEqualTo(executor)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setAnalysisQueueDepth() {
+        controller.imageAnalysisImageQueueDepth = 100
+        assertThat(controller.imageAnalysisImageQueueDepth).isEqualTo(100)
+        assertThat(controller.mImageAnalysis.imageQueueDepth).isEqualTo(100)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setAnalysisBackpressureStrategy() {
+        controller.imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_BLOCK_PRODUCER
+        assertThat(controller.imageAnalysisBackpressureStrategy)
+            .isEqualTo(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
+        assertThat(controller.mImageAnalysis.backpressureStrategy)
+            .isEqualTo(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setImageCaptureResolution() {
+        controller.imageCaptureTargetSize = targetSizeWithResolution
+        assertThat(controller.imageCaptureTargetSize).isEqualTo(targetSizeWithResolution)
+
+        val config = controller.mImageCapture.currentConfig as ImageOutputConfig
+        assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setImageCaptureAspectRatio() {
+        controller.imageCaptureTargetSize = targetSizeWithAspectRatio
+        assertThat(controller.imageCaptureTargetSize).isEqualTo(targetSizeWithAspectRatio)
+
+        val config = controller.mImageCapture.currentConfig as ImageOutputConfig
+        assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setImageCaptureMode() {
+        controller.imageCaptureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
+        assertThat(controller.imageCaptureMode)
+            .isEqualTo(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+        assertThat(controller.mImageCapture.captureMode)
+            .isEqualTo(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setImageCaptureIoExecutor() {
+        val ioExecutor = Executors.newSingleThreadExecutor()
+        controller.imageCaptureIoExecutor = ioExecutor
+        assertThat(controller.imageCaptureIoExecutor).isEqualTo(ioExecutor)
+        val config = controller.mImageCapture.currentConfig as ImageCaptureConfig
+        assertThat(config.ioExecutor).isEqualTo(ioExecutor)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setVideoCaptureResolution() {
+        controller.videoCaptureTargetSize = targetSizeWithResolution
+        assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithResolution)
+
+        val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
+        assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun setVideoCaptureAspectRatio() {
+        controller.videoCaptureTargetSize = targetSizeWithAspectRatio
+        assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithAspectRatio)
+
+        val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
+        assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+    }
+
+    @UiThreadTest
+    @Test
+    public fun sensorRotationChanges_useCaseTargetRotationUpdated() {
         // Act.
         controller.mRotationReceiver.onRotationChanged(Surface.ROTATION_180)
 
         // Assert.
         assertThat(controller.mImageAnalysis.targetRotation).isEqualTo(Surface.ROTATION_180)
         assertThat(controller.mImageCapture.targetRotation).isEqualTo(Surface.ROTATION_180)
-        // TODO(b/177276479): verify VideoCapture once it supports getTargetRotation().
+        val videoConfig = controller.mVideoCapture.currentConfig as ImageOutputConfig
+        assertThat(videoConfig.targetRotation).isEqualTo(Surface.ROTATION_180)
     }
 
     @UiThreadTest
     @Test
     public fun setSelectorBeforeBound_selectorSet() {
         // Arrange.
-        val controller = LifecycleCameraController(context)
         assertThat(controller.cameraSelector.lensFacing).isEqualTo(CameraSelector.LENS_FACING_BACK)
 
         // Act.
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 5ee2e66..e1ea9bb 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -118,6 +118,14 @@
     }
 
     @Test
+    fun controllerBound_canGetCameraControl() {
+        fragment.assertPreviewIsStreaming()
+        instrumentation.runOnMainSync {
+            assertThat(fragment.cameraController.cameraControl).isNotNull()
+        }
+    }
+
+    @Test
     fun controllerBound_canGetCameraInfo() {
         fragment.assertPreviewIsStreaming()
         instrumentation.runOnMainSync {