[camerax] Wrap methods necessary for preview implementation (#7046)

* Add code needed from proof of concept

* Add test files, delete unecessary method

* Add tests, remove unecessary code

* Fix analyze

* Update changelog

* Cleanup:

* Cleanup and add switch

* Finish todo

* Add onCameraError

* Fix pigeon file

* Add method for releasing flutter texture and cleanup surface logic

* Add test for release method

* Add dart test

* Update changelog

* Modify flutter api names to avoid stack overflow

* Cleanup

* Fix tests

* Delete space

* Address review 1

* Update switch

* Add annotations and constants in tests

* Reset verification behavior
diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md
index d5355c6..080240a 100644
--- a/packages/camera/camera_android_camerax/CHANGELOG.md
+++ b/packages/camera/camera_android_camerax/CHANGELOG.md
@@ -8,3 +8,4 @@
 * Adds Camera and UseCase classes, along with methods for binding UseCases to a lifecycle with the ProcessCameraProvider.
 * Bump CameraX version to 1.3.0-alpha03 and Kotlin version to 1.8.0.
 * Changes instance manager to allow the separate creation of identical objects.
+* Adds Preview and Surface classes, along with other methods needed to implement camera preview.
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java
index c35394f..b61e7ac 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java
@@ -49,6 +49,8 @@
         binaryMessenger, processCameraProviderHostApi);
     systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
     GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi);
+    GeneratedCameraXLibrary.PreviewHostApi.setup(
+        binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
   }
 
   @Override
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java
index 83c43a9..4a3d277 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java
@@ -5,8 +5,14 @@
 package io.flutter.plugins.camerax;
 
 import android.app.Activity;
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+import androidx.annotation.NonNull;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.Preview;
+import io.flutter.plugin.common.BinaryMessenger;
 
+/** Utility class used to create CameraX-related objects primarily for testing purposes. */
 public class CameraXProxy {
   public CameraSelector.Builder createCameraSelectorBuilder() {
     return new CameraSelector.Builder();
@@ -17,10 +23,29 @@
   }
 
   public DeviceOrientationManager createDeviceOrientationManager(
-      Activity activity,
-      Boolean isFrontFacing,
-      int sensorOrientation,
-      DeviceOrientationManager.DeviceOrientationChangeCallback callback) {
+      @NonNull Activity activity,
+      @NonNull Boolean isFrontFacing,
+      @NonNull int sensorOrientation,
+      @NonNull DeviceOrientationManager.DeviceOrientationChangeCallback callback) {
     return new DeviceOrientationManager(activity, isFrontFacing, sensorOrientation, callback);
   }
+
+  public Preview.Builder createPreviewBuilder() {
+    return new Preview.Builder();
+  }
+
+  public Surface createSurface(@NonNull SurfaceTexture surfaceTexture) {
+    return new Surface(surfaceTexture);
+  }
+
+  /**
+   * Creates an instance of the {@code SystemServicesFlutterApiImpl}.
+   *
+   * <p>Included in this class to utilize the callback methods it provides, e.g. {@code
+   * onCameraError(String)}.
+   */
+  public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl(
+      @NonNull BinaryMessenger binaryMessenger) {
+    return new SystemServicesFlutterApiImpl(binaryMessenger);
+  }
 }
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
index 528870c..1e61ea6 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
@@ -26,6 +26,82 @@
 public class GeneratedCameraXLibrary {
 
   /** Generated class from Pigeon that represents data sent in messages. */
+  public static class ResolutionInfo {
+    private @NonNull Long width;
+
+    public @NonNull Long getWidth() {
+      return width;
+    }
+
+    public void setWidth(@NonNull Long setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"width\" is null.");
+      }
+      this.width = setterArg;
+    }
+
+    private @NonNull Long height;
+
+    public @NonNull Long getHeight() {
+      return height;
+    }
+
+    public void setHeight(@NonNull Long setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"height\" is null.");
+      }
+      this.height = setterArg;
+    }
+
+    /** Constructor is private to enforce null safety; use Builder. */
+    private ResolutionInfo() {}
+
+    public static final class Builder {
+      private @Nullable Long width;
+
+      public @NonNull Builder setWidth(@NonNull Long setterArg) {
+        this.width = setterArg;
+        return this;
+      }
+
+      private @Nullable Long height;
+
+      public @NonNull Builder setHeight(@NonNull Long setterArg) {
+        this.height = setterArg;
+        return this;
+      }
+
+      public @NonNull ResolutionInfo build() {
+        ResolutionInfo pigeonReturn = new ResolutionInfo();
+        pigeonReturn.setWidth(width);
+        pigeonReturn.setHeight(height);
+        return pigeonReturn;
+      }
+    }
+
+    @NonNull
+    Map<String, Object> toMap() {
+      Map<String, Object> toMapResult = new HashMap<>();
+      toMapResult.put("width", width);
+      toMapResult.put("height", height);
+      return toMapResult;
+    }
+
+    static @NonNull ResolutionInfo fromMap(@NonNull Map<String, Object> map) {
+      ResolutionInfo pigeonResult = new ResolutionInfo();
+      Object width = map.get("width");
+      pigeonResult.setWidth(
+          (width == null) ? null : ((width instanceof Integer) ? (Integer) width : (Long) width));
+      Object height = map.get("height");
+      pigeonResult.setHeight(
+          (height == null)
+              ? null
+              : ((height instanceof Integer) ? (Integer) height : (Long) height));
+      return pigeonResult;
+    }
+  }
+
+  /** Generated class from Pigeon that represents data sent in messages. */
   public static class CameraPermissionsErrorData {
     private @NonNull String errorCode;
 
@@ -843,6 +919,185 @@
             callback.reply(null);
           });
     }
+
+    public void onCameraError(@NonNull String errorDescriptionArg, Reply<Void> callback) {
+      BasicMessageChannel<Object> channel =
+          new BasicMessageChannel<>(
+              binaryMessenger,
+              "dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError",
+              getCodec());
+      channel.send(
+          new ArrayList<Object>(Arrays.asList(errorDescriptionArg)),
+          channelReply -> {
+            callback.reply(null);
+          });
+    }
+  }
+
+  private static class PreviewHostApiCodec extends StandardMessageCodec {
+    public static final PreviewHostApiCodec INSTANCE = new PreviewHostApiCodec();
+
+    private PreviewHostApiCodec() {}
+
+    @Override
+    protected Object readValueOfType(byte type, ByteBuffer buffer) {
+      switch (type) {
+        case (byte) 128:
+          return ResolutionInfo.fromMap((Map<String, Object>) readValue(buffer));
+
+        case (byte) 129:
+          return ResolutionInfo.fromMap((Map<String, Object>) readValue(buffer));
+
+        default:
+          return super.readValueOfType(type, buffer);
+      }
+    }
+
+    @Override
+    protected void writeValue(ByteArrayOutputStream stream, Object value) {
+      if (value instanceof ResolutionInfo) {
+        stream.write(128);
+        writeValue(stream, ((ResolutionInfo) value).toMap());
+      } else if (value instanceof ResolutionInfo) {
+        stream.write(129);
+        writeValue(stream, ((ResolutionInfo) value).toMap());
+      } else {
+        super.writeValue(stream, value);
+      }
+    }
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+  public interface PreviewHostApi {
+    void create(
+        @NonNull Long identifier,
+        @Nullable Long rotation,
+        @Nullable ResolutionInfo targetResolution);
+
+    @NonNull
+    Long setSurfaceProvider(@NonNull Long identifier);
+
+    void releaseFlutterSurfaceTexture();
+
+    @NonNull
+    ResolutionInfo getResolutionInfo(@NonNull Long identifier);
+
+    /** The codec used by PreviewHostApi. */
+    static MessageCodec<Object> getCodec() {
+      return PreviewHostApiCodec.INSTANCE;
+    }
+
+    /** Sets up an instance of `PreviewHostApi` to handle messages through the `binaryMessenger`. */
+    static void setup(BinaryMessenger binaryMessenger, PreviewHostApi api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.create", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  ArrayList<Object> args = (ArrayList<Object>) message;
+                  Number identifierArg = (Number) args.get(0);
+                  if (identifierArg == null) {
+                    throw new NullPointerException("identifierArg unexpectedly null.");
+                  }
+                  Number rotationArg = (Number) args.get(1);
+                  ResolutionInfo targetResolutionArg = (ResolutionInfo) args.get(2);
+                  api.create(
+                      (identifierArg == null) ? null : identifierArg.longValue(),
+                      (rotationArg == null) ? null : rotationArg.longValue(),
+                      targetResolutionArg);
+                  wrapped.put("result", null);
+                } catch (Error | RuntimeException exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger,
+                "dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider",
+                getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  ArrayList<Object> args = (ArrayList<Object>) message;
+                  Number identifierArg = (Number) args.get(0);
+                  if (identifierArg == null) {
+                    throw new NullPointerException("identifierArg unexpectedly null.");
+                  }
+                  Long output =
+                      api.setSurfaceProvider(
+                          (identifierArg == null) ? null : identifierArg.longValue());
+                  wrapped.put("result", output);
+                } catch (Error | RuntimeException exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger,
+                "dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture",
+                getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  api.releaseFlutterSurfaceTexture();
+                  wrapped.put("result", null);
+                } catch (Error | RuntimeException exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.getResolutionInfo", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  ArrayList<Object> args = (ArrayList<Object>) message;
+                  Number identifierArg = (Number) args.get(0);
+                  if (identifierArg == null) {
+                    throw new NullPointerException("identifierArg unexpectedly null.");
+                  }
+                  ResolutionInfo output =
+                      api.getResolutionInfo(
+                          (identifierArg == null) ? null : identifierArg.longValue());
+                  wrapped.put("result", output);
+                } catch (Error | RuntimeException exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
   }
 
   private static Map<String, Object> wrapError(Throwable exception) {
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java
new file mode 100644
index 0000000..838f0b3
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java
@@ -0,0 +1,149 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camerax;
+
+import android.graphics.SurfaceTexture;
+import android.util.Size;
+import android.view.Surface;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.Preview;
+import androidx.camera.core.SurfaceRequest;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PreviewHostApi;
+import io.flutter.view.TextureRegistry;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+
+public class PreviewHostApiImpl implements PreviewHostApi {
+  private final BinaryMessenger binaryMessenger;
+  private final InstanceManager instanceManager;
+  private final TextureRegistry textureRegistry;
+
+  @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy();
+  @VisibleForTesting public TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture;
+
+  public PreviewHostApiImpl(
+      @NonNull BinaryMessenger binaryMessenger,
+      @NonNull InstanceManager instanceManager,
+      @NonNull TextureRegistry textureRegistry) {
+    this.binaryMessenger = binaryMessenger;
+    this.instanceManager = instanceManager;
+    this.textureRegistry = textureRegistry;
+  }
+
+  /** Creates a {@link Preview} with the target rotation and resolution if specified. */
+  @Override
+  public void create(
+      @NonNull Long identifier,
+      @Nullable Long rotation,
+      @Nullable GeneratedCameraXLibrary.ResolutionInfo targetResolution) {
+    Preview.Builder previewBuilder = cameraXProxy.createPreviewBuilder();
+    if (rotation != null) {
+      previewBuilder.setTargetRotation(rotation.intValue());
+    }
+    if (targetResolution != null) {
+      previewBuilder.setTargetResolution(
+          new Size(
+              targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue()));
+    }
+    Preview preview = previewBuilder.build();
+    instanceManager.addDartCreatedInstance(preview, identifier);
+  }
+
+  /**
+   * Sets the {@link Preview.SurfaceProvider} that will be used to provide a {@code Surface} backed
+   * by a Flutter {@link TextureRegistry.SurfaceTextureEntry} used to build the {@link Preview}.
+   */
+  @Override
+  public Long setSurfaceProvider(@NonNull Long identifier) {
+    Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier));
+    flutterSurfaceTexture = textureRegistry.createSurfaceTexture();
+    SurfaceTexture surfaceTexture = flutterSurfaceTexture.surfaceTexture();
+    Preview.SurfaceProvider surfaceProvider = createSurfaceProvider(surfaceTexture);
+    preview.setSurfaceProvider(surfaceProvider);
+
+    return flutterSurfaceTexture.id();
+  }
+
+  /**
+   * Creates a {@link Preview.SurfaceProvider} that specifies how to provide a {@link Surface} to a
+   * {@code Preview} that is backed by a Flutter {@link TextureRegistry.SurfaceTextureEntry}.
+   */
+  @VisibleForTesting
+  public Preview.SurfaceProvider createSurfaceProvider(@NonNull SurfaceTexture surfaceTexture) {
+    return new Preview.SurfaceProvider() {
+      @Override
+      public void onSurfaceRequested(SurfaceRequest request) {
+        surfaceTexture.setDefaultBufferSize(
+            request.getResolution().getWidth(), request.getResolution().getHeight());
+        Surface flutterSurface = cameraXProxy.createSurface(surfaceTexture);
+        request.provideSurface(
+            flutterSurface,
+            Executors.newSingleThreadExecutor(),
+            (result) -> {
+              // See https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result for documentation.
+              // Always attempt a release.
+              flutterSurface.release();
+              int resultCode = result.getResultCode();
+              switch (resultCode) {
+                case SurfaceRequest.Result.RESULT_REQUEST_CANCELLED:
+                case SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE:
+                case SurfaceRequest.Result.RESULT_SURFACE_ALREADY_PROVIDED:
+                case SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY:
+                  // Only need to release, do nothing.
+                  break;
+                case SurfaceRequest.Result.RESULT_INVALID_SURFACE: // Intentional fall through.
+                default:
+                  // Release and send error.
+                  SystemServicesFlutterApiImpl systemServicesFlutterApi =
+                      cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger);
+                  systemServicesFlutterApi.sendCameraError(
+                      getProvideSurfaceErrorDescription(resultCode), reply -> {});
+                  break;
+              }
+            });
+      };
+    };
+  }
+
+  /**
+   * Returns an error description for each {@link SurfaceRequest.Result} that represents an error
+   * with providing a surface.
+   */
+  private String getProvideSurfaceErrorDescription(@Nullable int resultCode) {
+    switch (resultCode) {
+      case SurfaceRequest.Result.RESULT_INVALID_SURFACE:
+        return resultCode + ": Provided surface could not be used by the camera.";
+      default:
+        return resultCode + ": Attempt to provide a surface resulted with unrecognizable code.";
+    }
+  }
+
+  /**
+   * Releases the Flutter {@link TextureRegistry.SurfaceTextureEntry} if used to provide a surface
+   * for a {@link Preview}.
+   */
+  @Override
+  public void releaseFlutterSurfaceTexture() {
+    if (flutterSurfaceTexture != null) {
+      flutterSurfaceTexture.release();
+    }
+  }
+
+  /** Returns the resolution information for the specified {@link Preview}. */
+  @Override
+  public GeneratedCameraXLibrary.ResolutionInfo getResolutionInfo(@NonNull Long identifier) {
+    Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier));
+    Size resolution = preview.getResolutionInfo().getResolution();
+
+    GeneratedCameraXLibrary.ResolutionInfo.Builder resolutionInfo =
+        new GeneratedCameraXLibrary.ResolutionInfo.Builder()
+            .setWidth(Long.valueOf(resolution.getWidth()))
+            .setHeight(Long.valueOf(resolution.getHeight()));
+    return resolutionInfo.build();
+  }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java
index 1e9f33b..6315897 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java
@@ -4,19 +4,21 @@
 
 package io.flutter.plugins.camerax;
 
+import androidx.annotation.NonNull;
 import io.flutter.plugin.common.BinaryMessenger;
 import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi;
 
 public class SystemServicesFlutterApiImpl extends SystemServicesFlutterApi {
-  public SystemServicesFlutterApiImpl(
-      BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
+  public SystemServicesFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) {
     super(binaryMessenger);
-    this.instanceManager = instanceManager;
   }
 
-  private final InstanceManager instanceManager;
-
-  public void onDeviceOrientationChanged(String orientation, Reply<Void> reply) {
+  public void sendDeviceOrientationChangedEvent(
+      @NonNull String orientation, @NonNull Reply<Void> reply) {
     super.onDeviceOrientationChanged(orientation, reply);
   }
+
+  public void sendCameraError(@NonNull String errorDescription, @NonNull Reply<Void> reply) {
+    super.onCameraError(errorDescription, reply);
+  }
 }
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
index e8eb715..a698581 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
@@ -28,8 +28,7 @@
       BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
     this.binaryMessenger = binaryMessenger;
     this.instanceManager = instanceManager;
-    this.systemServicesFlutterApi =
-        new SystemServicesFlutterApiImpl(binaryMessenger, instanceManager);
+    this.systemServicesFlutterApi = new SystemServicesFlutterApiImpl(binaryMessenger);
   }
 
   public void setActivity(Activity activity) {
@@ -86,7 +85,7 @@
             isFrontFacing,
             sensorOrientation.intValue(),
             (DeviceOrientation newOrientation) -> {
-              systemServicesFlutterApi.onDeviceOrientationChanged(
+              systemServicesFlutterApi.sendDeviceOrientationChangedEvent(
                   serializeDeviceOrientation(newOrientation), reply -> {});
             });
     deviceOrientationManager.start();
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java
new file mode 100644
index 0000000..9cb4e91
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java
@@ -0,0 +1,221 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camerax;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.SurfaceTexture;
+import android.util.Size;
+import android.view.Surface;
+import androidx.camera.core.Preview;
+import androidx.camera.core.SurfaceRequest;
+import androidx.core.util.Consumer;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply;
+import io.flutter.view.TextureRegistry;
+import java.util.concurrent.Executor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class PreviewTest {
+  @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+  @Mock public Preview mockPreview;
+  @Mock public BinaryMessenger mockBinaryMessenger;
+  @Mock public TextureRegistry mockTextureRegistry;
+  @Mock public CameraXProxy mockCameraXProxy;
+
+  InstanceManager testInstanceManager;
+
+  @Before
+  public void setUp() {
+    testInstanceManager = spy(InstanceManager.open(identifier -> {}));
+  }
+
+  @After
+  public void tearDown() {
+    testInstanceManager.close();
+  }
+
+  @Test
+  public void create_createsPreviewWithCorrectConfiguration() {
+    final PreviewHostApiImpl previewHostApi =
+        new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+    final Preview.Builder mockPreviewBuilder = mock(Preview.Builder.class);
+    final int targetRotation = 90;
+    final int targetResolutionWidth = 10;
+    final int targetResolutionHeight = 50;
+    final Long previewIdentifier = 3L;
+    final GeneratedCameraXLibrary.ResolutionInfo resolutionInfo =
+        new GeneratedCameraXLibrary.ResolutionInfo.Builder()
+            .setWidth(Long.valueOf(targetResolutionWidth))
+            .setHeight(Long.valueOf(targetResolutionHeight))
+            .build();
+
+    previewHostApi.cameraXProxy = mockCameraXProxy;
+    when(mockCameraXProxy.createPreviewBuilder()).thenReturn(mockPreviewBuilder);
+    when(mockPreviewBuilder.build()).thenReturn(mockPreview);
+
+    final ArgumentCaptor<Size> sizeCaptor = ArgumentCaptor.forClass(Size.class);
+
+    previewHostApi.create(previewIdentifier, Long.valueOf(targetRotation), resolutionInfo);
+
+    verify(mockPreviewBuilder).setTargetRotation(targetRotation);
+    verify(mockPreviewBuilder).setTargetResolution(sizeCaptor.capture());
+    assertEquals(sizeCaptor.getValue().getWidth(), targetResolutionWidth);
+    assertEquals(sizeCaptor.getValue().getHeight(), targetResolutionHeight);
+    verify(mockPreviewBuilder).build();
+    verify(testInstanceManager).addDartCreatedInstance(mockPreview, previewIdentifier);
+  }
+
+  @Test
+  public void setSurfaceProviderTest_createsSurfaceProviderAndReturnsTextureEntryId() {
+    final PreviewHostApiImpl previewHostApi =
+        spy(new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry));
+    final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry =
+        mock(TextureRegistry.SurfaceTextureEntry.class);
+    final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
+    final Long previewIdentifier = 5L;
+    final Long surfaceTextureEntryId = 120L;
+
+    previewHostApi.cameraXProxy = mockCameraXProxy;
+    testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier);
+
+    when(mockTextureRegistry.createSurfaceTexture()).thenReturn(mockSurfaceTextureEntry);
+    when(mockSurfaceTextureEntry.surfaceTexture()).thenReturn(mockSurfaceTexture);
+    when(mockSurfaceTextureEntry.id()).thenReturn(surfaceTextureEntryId);
+
+    final ArgumentCaptor<Preview.SurfaceProvider> surfaceProviderCaptor =
+        ArgumentCaptor.forClass(Preview.SurfaceProvider.class);
+    final ArgumentCaptor<Surface> surfaceCaptor = ArgumentCaptor.forClass(Surface.class);
+    final ArgumentCaptor<Consumer> consumerCaptor = ArgumentCaptor.forClass(Consumer.class);
+
+    // Test that surface provider was set and the surface texture ID was returned.
+    assertEquals(previewHostApi.setSurfaceProvider(previewIdentifier), surfaceTextureEntryId);
+    verify(mockPreview).setSurfaceProvider(surfaceProviderCaptor.capture());
+    verify(previewHostApi).createSurfaceProvider(mockSurfaceTexture);
+  }
+
+  @Test
+  public void createSurfaceProvider_createsExpectedPreviewSurfaceProvider() {
+    final PreviewHostApiImpl previewHostApi =
+        new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+    final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
+    final Surface mockSurface = mock(Surface.class);
+    final SurfaceRequest mockSurfaceRequest = mock(SurfaceRequest.class);
+    final SurfaceRequest.Result mockSurfaceRequestResult = mock(SurfaceRequest.Result.class);
+    final SystemServicesFlutterApiImpl mockSystemServicesFlutterApi =
+        mock(SystemServicesFlutterApiImpl.class);
+    final int resolutionWidth = 200;
+    final int resolutionHeight = 500;
+
+    previewHostApi.cameraXProxy = mockCameraXProxy;
+    when(mockCameraXProxy.createSurface(mockSurfaceTexture)).thenReturn(mockSurface);
+    when(mockSurfaceRequest.getResolution())
+        .thenReturn(new Size(resolutionWidth, resolutionHeight));
+    when(mockCameraXProxy.createSystemServicesFlutterApiImpl(mockBinaryMessenger))
+        .thenReturn(mockSystemServicesFlutterApi);
+
+    final ArgumentCaptor<Surface> surfaceCaptor = ArgumentCaptor.forClass(Surface.class);
+    final ArgumentCaptor<Consumer> consumerCaptor = ArgumentCaptor.forClass(Consumer.class);
+
+    Preview.SurfaceProvider previewSurfaceProvider =
+        previewHostApi.createSurfaceProvider(mockSurfaceTexture);
+    previewSurfaceProvider.onSurfaceRequested(mockSurfaceRequest);
+
+    verify(mockSurfaceTexture).setDefaultBufferSize(resolutionWidth, resolutionHeight);
+    verify(mockSurfaceRequest)
+        .provideSurface(surfaceCaptor.capture(), any(Executor.class), consumerCaptor.capture());
+
+    // Test that the surface derived from the surface texture entry will be provided to the surface request.
+    assertEquals(surfaceCaptor.getValue(), mockSurface);
+
+    // Test that the Consumer used to handle surface request result releases Flutter surface texture appropriately
+    // and sends camera errors appropriately.
+    Consumer<SurfaceRequest.Result> capturedConsumer = consumerCaptor.getValue();
+
+    // Case where Surface should be released.
+    when(mockSurfaceRequestResult.getResultCode())
+        .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED);
+    capturedConsumer.accept(mockSurfaceRequestResult);
+    verify(mockSurface).release();
+    reset(mockSurface);
+
+    when(mockSurfaceRequestResult.getResultCode())
+        .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED);
+    capturedConsumer.accept(mockSurfaceRequestResult);
+    verify(mockSurface).release();
+    reset(mockSurface);
+
+    when(mockSurfaceRequestResult.getResultCode())
+        .thenReturn(SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE);
+    capturedConsumer.accept(mockSurfaceRequestResult);
+    verify(mockSurface).release();
+    reset(mockSurface);
+
+    when(mockSurfaceRequestResult.getResultCode())
+        .thenReturn(SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY);
+    capturedConsumer.accept(mockSurfaceRequestResult);
+    verify(mockSurface).release();
+    reset(mockSurface);
+
+    // Case where error must be sent.
+    when(mockSurfaceRequestResult.getResultCode())
+        .thenReturn(SurfaceRequest.Result.RESULT_INVALID_SURFACE);
+    capturedConsumer.accept(mockSurfaceRequestResult);
+    verify(mockSurface).release();
+    verify(mockSystemServicesFlutterApi).sendCameraError(anyString(), any(Reply.class));
+  }
+
+  @Test
+  public void releaseFlutterSurfaceTexture_makesCallToReleaseFlutterSurfaceTexture() {
+    final PreviewHostApiImpl previewHostApi =
+        new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+    final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry =
+        mock(TextureRegistry.SurfaceTextureEntry.class);
+
+    previewHostApi.flutterSurfaceTexture = mockSurfaceTextureEntry;
+
+    previewHostApi.releaseFlutterSurfaceTexture();
+    verify(mockSurfaceTextureEntry).release();
+  }
+
+  @Test
+  public void getResolutionInfo_makesCallToRetrievePreviewResolutionInfo() {
+    final PreviewHostApiImpl previewHostApi =
+        new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+    final androidx.camera.core.ResolutionInfo mockResolutionInfo =
+        mock(androidx.camera.core.ResolutionInfo.class);
+    final Long previewIdentifier = 23L;
+    final int resolutionWidth = 500;
+    final int resolutionHeight = 200;
+
+    testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier);
+    when(mockPreview.getResolutionInfo()).thenReturn(mockResolutionInfo);
+    when(mockResolutionInfo.getResolution())
+        .thenReturn(new Size(resolutionWidth, resolutionHeight));
+
+    ResolutionInfo resolutionInfo = previewHostApi.getResolutionInfo(previewIdentifier);
+    assertEquals(resolutionInfo.getWidth(), Long.valueOf(resolutionWidth));
+    assertEquals(resolutionInfo.getHeight(), Long.valueOf(resolutionHeight));
+  }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
index d90c263..eb36c45 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
@@ -129,7 +129,8 @@
 
     deviceOrientationChangeCallback.onChange(DeviceOrientation.PORTRAIT_DOWN);
     verify(systemServicesFlutterApi)
-        .onDeviceOrientationChanged(eq("PORTRAIT_DOWN"), any(Reply.class));
+        .sendDeviceOrientationChangedEvent(
+            eq(DeviceOrientation.PORTRAIT_DOWN.toString()), any(Reply.class));
 
     // Test that the DeviceOrientationManager starts listening for device orientation changes.
     verify(mockDeviceOrientationManager).start();
diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
index 6d88699..1d315e5 100644
--- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
+++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
@@ -10,6 +10,31 @@
 import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
 import 'package:flutter/services.dart';
 
+class ResolutionInfo {
+  ResolutionInfo({
+    required this.width,
+    required this.height,
+  });
+
+  int width;
+  int height;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['width'] = width;
+    pigeonMap['height'] = height;
+    return pigeonMap;
+  }
+
+  static ResolutionInfo decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return ResolutionInfo(
+      width: pigeonMap['width']! as int,
+      height: pigeonMap['height']! as int,
+    );
+  }
+}
+
 class CameraPermissionsErrorData {
   CameraPermissionsErrorData({
     required this.errorCode,
@@ -634,6 +659,7 @@
   static const MessageCodec<Object?> codec = _SystemServicesFlutterApiCodec();
 
   void onDeviceOrientationChanged(String orientation);
+  void onCameraError(String errorDescription);
   static void setup(SystemServicesFlutterApi? api,
       {BinaryMessenger? binaryMessenger}) {
     {
@@ -656,5 +682,174 @@
         });
       }
     }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final String? arg_errorDescription = (args[0] as String?);
+          assert(arg_errorDescription != null,
+              'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null, expected non-null String.');
+          api.onCameraError(arg_errorDescription!);
+          return;
+        });
+      }
+    }
+  }
+}
+
+class _PreviewHostApiCodec extends StandardMessageCodec {
+  const _PreviewHostApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is ResolutionInfo) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else if (value is ResolutionInfo) {
+      buffer.putUint8(129);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return ResolutionInfo.decode(readValue(buffer)!);
+
+      case 129:
+        return ResolutionInfo.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+class PreviewHostApi {
+  /// Constructor for [PreviewHostApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  PreviewHostApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _PreviewHostApiCodec();
+
+  Future<void> create(int arg_identifier, int? arg_rotation,
+      ResolutionInfo? arg_targetResolution) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.PreviewHostApi.create', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap = await channel
+            .send(<Object?>[arg_identifier, arg_rotation, arg_targetResolution])
+        as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+
+  Future<int> setSurfaceProvider(int arg_identifier) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_identifier]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyMap['result'] as int?)!;
+    }
+  }
+
+  Future<void> releaseFlutterSurfaceTexture() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(null) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+
+  Future<ResolutionInfo> getResolutionInfo(int arg_identifier) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_identifier]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyMap['result'] as ResolutionInfo?)!;
+    }
   }
 }
diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart
new file mode 100644
index 0000000..602bcb3
--- /dev/null
+++ b/packages/camera/camera_android_camerax/lib/src/preview.dart
@@ -0,0 +1,126 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/services.dart' show BinaryMessenger;
+
+import 'camerax_library.g.dart';
+import 'instance_manager.dart';
+import 'java_object.dart';
+import 'use_case.dart';
+
+/// Use case that provides a camera preview stream for display.
+///
+/// See https://developer.android.com/reference/androidx/camera/core/Preview.
+class Preview extends UseCase {
+  /// Creates a [Preview].
+  Preview(
+      {BinaryMessenger? binaryMessenger,
+      InstanceManager? instanceManager,
+      this.targetRotation,
+      this.targetResolution})
+      : super.detached(
+            binaryMessenger: binaryMessenger,
+            instanceManager: instanceManager) {
+    _api = PreviewHostApiImpl(
+        binaryMessenger: binaryMessenger, instanceManager: instanceManager);
+    _api.createFromInstance(this, targetRotation, targetResolution);
+  }
+
+  /// Constructs a [Preview] that is not automatically attached to a native object.
+  Preview.detached(
+      {BinaryMessenger? binaryMessenger,
+      InstanceManager? instanceManager,
+      this.targetRotation,
+      this.targetResolution})
+      : super.detached(
+            binaryMessenger: binaryMessenger,
+            instanceManager: instanceManager) {
+    _api = PreviewHostApiImpl(
+        binaryMessenger: binaryMessenger, instanceManager: instanceManager);
+  }
+
+  late final PreviewHostApiImpl _api;
+
+  /// Target rotation of the camera used for the preview stream.
+  final int? targetRotation;
+
+  /// Target resolution of the camera preview stream.
+  final ResolutionInfo? targetResolution;
+
+  /// Sets the surface provider for the preview stream.
+  ///
+  /// Returns the ID of the FlutterSurfaceTextureEntry used on the native end
+  /// used to display the preview stream on a [Texture] of the same ID.
+  Future<int> setSurfaceProvider() {
+    return _api.setSurfaceProviderFromInstance(this);
+  }
+
+  /// Releases Flutter surface texture used to provide a surface for the preview
+  /// stream.
+  void releaseFlutterSurfaceTexture() {
+    _api.releaseFlutterSurfaceTextureFromInstance();
+  }
+
+  /// Retrieves the selected resolution information of this [Preview].
+  Future<ResolutionInfo> getResolutionInfo() {
+    return _api.getResolutionInfoFromInstance(this);
+  }
+}
+
+/// Host API implementation of [Preview].
+class PreviewHostApiImpl extends PreviewHostApi {
+  /// Constructs a [PreviewHostApiImpl].
+  PreviewHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) {
+    this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
+  }
+
+  /// Receives binary data across the Flutter platform barrier.
+  ///
+  /// If it is null, the default BinaryMessenger will be used which routes to
+  /// the host platform.
+  final BinaryMessenger? binaryMessenger;
+
+  /// Maintains instances stored to communicate with native language objects.
+  late final InstanceManager instanceManager;
+
+  /// Creates a [Preview] with the target rotation provided if specified.
+  void createFromInstance(
+      Preview instance, int? targetRotation, ResolutionInfo? targetResolution) {
+    final int identifier = instanceManager.addDartCreatedInstance(instance,
+        onCopy: (Preview original) {
+      return Preview.detached(
+          binaryMessenger: binaryMessenger,
+          instanceManager: instanceManager,
+          targetRotation: original.targetRotation);
+    });
+    create(identifier, targetRotation, targetResolution);
+  }
+
+  /// Sets the surface provider of the specified [Preview] instance and returns
+  /// the ID corresponding to the surface it will provide.
+  Future<int> setSurfaceProviderFromInstance(Preview instance) async {
+    final int? identifier = instanceManager.getIdentifier(instance);
+    assert(identifier != null,
+        'No Preview has the identifer of that requested to set the surface provider on.');
+
+    final int surfaceTextureEntryId = await setSurfaceProvider(identifier!);
+    return surfaceTextureEntryId;
+  }
+
+  /// Releases Flutter surface texture used to provide a surface for the preview
+  /// stream if a surface provider was set for a [Preview] instance.
+  void releaseFlutterSurfaceTextureFromInstance() {
+    releaseFlutterSurfaceTexture();
+  }
+
+  /// Gets the resolution information of the specified [Preview] instance.
+  Future<ResolutionInfo> getResolutionInfoFromInstance(Preview instance) async {
+    final int? identifier = instanceManager.getIdentifier(instance);
+    assert(identifier != null,
+        'No Preview has the identifer of that requested to get the resolution information for.');
+
+    final ResolutionInfo resolutionInfo = await getResolutionInfo(identifier!);
+    return resolutionInfo;
+  }
+}
diff --git a/packages/camera/camera_android_camerax/lib/src/surface.dart b/packages/camera/camera_android_camerax/lib/src/surface.dart
new file mode 100644
index 0000000..ea8cf8c
--- /dev/null
+++ b/packages/camera/camera_android_camerax/lib/src/surface.dart
@@ -0,0 +1,34 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'java_object.dart';
+
+/// Handle onto the raw buffer managed by screen compositor.
+///
+/// See https://developer.android.com/reference/android/view/Surface.html.
+class Surface extends JavaObject {
+  /// Creates a detached [UseCase].
+  Surface.detached({super.binaryMessenger, super.instanceManager})
+      : super.detached();
+
+  /// Rotation constant to signify the natural orientation.
+  ///
+  /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_0.
+  static const int ROTATION_0 = 0;
+
+  /// Rotation constant to signify a 90 degrees rotation.
+  ///
+  /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_90.
+  static const int ROTATION_90 = 1;
+
+  /// Rotation constant to signify a 180 degrees rotation.
+  ///
+  /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_180.
+  static const int ROTATION_180 = 2;
+
+  /// Rotation constant to signify a 270 degrees rotation.
+  ///
+  /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_270.
+  static const int ROTATION_270 = 3;
+}
diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart
index bc6477e..4ca90e2 100644
--- a/packages/camera/camera_android_camerax/lib/src/system_services.dart
+++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart
@@ -16,7 +16,7 @@
 // ignore_for_file: avoid_classes_with_only_static_members
 
 /// Utility class that offers access to Android system services needed for
-/// camera usage.
+/// camera usage and other informational streams.
 class SystemServices {
   /// Stream that emits the device orientation whenever it is changed.
   ///
@@ -26,6 +26,10 @@
       deviceOrientationChangedStreamController =
       StreamController<DeviceOrientationChangedEvent>.broadcast();
 
+  /// Stream that emits the errors caused by camera usage on the native side.
+  static final StreamController<String> cameraErrorStreamController =
+      StreamController<String>.broadcast();
+
   /// Requests permission to access the camera and audio if specified.
   static Future<void> requestCameraPermissions(bool enableAudio,
       {BinaryMessenger? binaryMessenger}) {
@@ -134,4 +138,12 @@
             '"$orientation" is not a valid DeviceOrientation value');
     }
   }
+
+  /// Callback method for any errors caused by camera usage on the Java side.
+  @override
+  void onCameraError(String errorDescription) {
+    // TODO(camsim99): Use this to implement onCameraError method in plugin.
+    // See https://github.com/flutter/flutter/issues/119571 for context.
+    SystemServices.cameraErrorStreamController.add(errorDescription);
+  }
 }
diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
index 7fce6ce..4172cd7 100644
--- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
+++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
@@ -26,6 +26,16 @@
     ),
   ),
 )
+class ResolutionInfo {
+  ResolutionInfo({
+    required this.width,
+    required this.height,
+  });
+
+  int width;
+  int height;
+}
+
 class CameraPermissionsErrorData {
   CameraPermissionsErrorData({
     required this.errorCode,
@@ -107,4 +117,17 @@
 @FlutterApi()
 abstract class SystemServicesFlutterApi {
   void onDeviceOrientationChanged(String orientation);
+
+  void onCameraError(String errorDescription);
+}
+
+@HostApi(dartHostTestHandler: 'TestPreviewHostApi')
+abstract class PreviewHostApi {
+  void create(int identifier, int? rotation, ResolutionInfo? targetResolution);
+
+  int setSurfaceProvider(int identifier);
+
+  void releaseFlutterSurfaceTexture();
+
+  ResolutionInfo getResolutionInfo(int identifier);
 }
diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart
new file mode 100644
index 0000000..36b56f0
--- /dev/null
+++ b/packages/camera/camera_android_camerax/test/preview_test.dart
@@ -0,0 +1,138 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:camera_android_camerax/src/camerax_library.g.dart';
+import 'package:camera_android_camerax/src/instance_manager.dart';
+import 'package:camera_android_camerax/src/preview.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import 'preview_test.mocks.dart';
+import 'test_camerax_library.g.dart';
+
+@GenerateMocks(<Type>[TestPreviewHostApi])
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('Preview', () {
+    tearDown(() => TestPreviewHostApi.setup(null));
+
+    test('detached create does not call create on the Java side', () async {
+      final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+      TestPreviewHostApi.setup(mockApi);
+
+      final InstanceManager instanceManager = InstanceManager(
+        onWeakReferenceRemoved: (_) {},
+      );
+      Preview.detached(
+        instanceManager: instanceManager,
+        targetRotation: 90,
+        targetResolution: ResolutionInfo(width: 50, height: 10),
+      );
+
+      verifyNever(mockApi.create(argThat(isA<int>()), argThat(isA<int>()),
+          argThat(isA<ResolutionInfo>())));
+    });
+
+    test('create calls create on the Java side', () async {
+      final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+      TestPreviewHostApi.setup(mockApi);
+
+      final InstanceManager instanceManager = InstanceManager(
+        onWeakReferenceRemoved: (_) {},
+      );
+      const int targetRotation = 90;
+      const int targetResolutionWidth = 10;
+      const int targetResolutionHeight = 50;
+      Preview(
+        instanceManager: instanceManager,
+        targetRotation: targetRotation,
+        targetResolution: ResolutionInfo(
+            width: targetResolutionWidth, height: targetResolutionHeight),
+      );
+
+      final VerificationResult createVerification = verify(mockApi.create(
+          argThat(isA<int>()), argThat(equals(targetRotation)), captureAny));
+      final ResolutionInfo capturedResolutionInfo =
+          createVerification.captured.single as ResolutionInfo;
+      expect(capturedResolutionInfo.width, equals(targetResolutionWidth));
+      expect(capturedResolutionInfo.height, equals(targetResolutionHeight));
+    });
+
+    test(
+        'setSurfaceProvider makes call to set surface provider for preview instance',
+        () async {
+      final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+      TestPreviewHostApi.setup(mockApi);
+
+      final InstanceManager instanceManager = InstanceManager(
+        onWeakReferenceRemoved: (_) {},
+      );
+      const int textureId = 8;
+      final Preview preview = Preview.detached(
+        instanceManager: instanceManager,
+      );
+      instanceManager.addHostCreatedInstance(
+        preview,
+        0,
+        onCopy: (_) => Preview.detached(),
+      );
+
+      when(mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview)))
+          .thenReturn(textureId);
+      expect(await preview.setSurfaceProvider(), equals(textureId));
+
+      verify(
+          mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview)));
+    });
+
+    test(
+        'releaseFlutterSurfaceTexture makes call to relase flutter surface texture entry',
+        () async {
+      final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+      TestPreviewHostApi.setup(mockApi);
+
+      final Preview preview = Preview.detached();
+
+      preview.releaseFlutterSurfaceTexture();
+
+      verify(mockApi.releaseFlutterSurfaceTexture());
+    });
+
+    test(
+        'getResolutionInfo makes call to get resolution information for preview instance',
+        () async {
+      final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+      TestPreviewHostApi.setup(mockApi);
+
+      final InstanceManager instanceManager = InstanceManager(
+        onWeakReferenceRemoved: (_) {},
+      );
+      final Preview preview = Preview.detached(
+        instanceManager: instanceManager,
+      );
+      const int resolutionWidth = 10;
+      const int resolutionHeight = 60;
+      final ResolutionInfo testResolutionInfo =
+          ResolutionInfo(width: resolutionWidth, height: resolutionHeight);
+
+      instanceManager.addHostCreatedInstance(
+        preview,
+        0,
+        onCopy: (_) => Preview.detached(),
+      );
+
+      when(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview)))
+          .thenReturn(testResolutionInfo);
+
+      final ResolutionInfo previewResolutionInfo =
+          await preview.getResolutionInfo();
+      expect(previewResolutionInfo.width, equals(resolutionWidth));
+      expect(previewResolutionInfo.height, equals(resolutionHeight));
+
+      verify(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview)));
+    });
+  });
+}
diff --git a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart
new file mode 100644
index 0000000..60fa152
--- /dev/null
+++ b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart
@@ -0,0 +1,89 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in camera_android_camerax/test/preview_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+
+import 'test_camerax_library.g.dart' as _i3;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeResolutionInfo_0 extends _i1.SmartFake
+    implements _i2.ResolutionInfo {
+  _FakeResolutionInfo_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [TestPreviewHostApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTestPreviewHostApi extends _i1.Mock
+    implements _i3.TestPreviewHostApi {
+  MockTestPreviewHostApi() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  void create(
+    int? identifier,
+    int? rotation,
+    _i2.ResolutionInfo? targetResolution,
+  ) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #create,
+          [
+            identifier,
+            rotation,
+            targetResolution,
+          ],
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
+  int setSurfaceProvider(int? identifier) => (super.noSuchMethod(
+        Invocation.method(
+          #setSurfaceProvider,
+          [identifier],
+        ),
+        returnValue: 0,
+      ) as int);
+  @override
+  void releaseFlutterSurfaceTexture() => super.noSuchMethod(
+        Invocation.method(
+          #releaseFlutterSurfaceTexture,
+          [],
+        ),
+        returnValueForMissingStub: null,
+      );
+  @override
+  _i2.ResolutionInfo getResolutionInfo(int? identifier) => (super.noSuchMethod(
+        Invocation.method(
+          #getResolutionInfo,
+          [identifier],
+        ),
+        returnValue: _FakeResolutionInfo_0(
+          this,
+          Invocation.method(
+            #getResolutionInfo,
+            [identifier],
+          ),
+        ),
+      ) as _i2.ResolutionInfo);
+}
diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart
index 2d2cea6..38037ea 100644
--- a/packages/camera/camera_android_camerax/test/system_services_test.dart
+++ b/packages/camera/camera_android_camerax/test/system_services_test.dart
@@ -97,5 +97,14 @@
               'message',
               '"FAKE_ORIENTATION" is not a valid DeviceOrientation value')));
     });
+
+    test('onCameraError adds new error to stream', () {
+      const String testErrorDescription = 'Test error description!';
+      SystemServices.cameraErrorStreamController.stream
+          .listen((String errorDescription) {
+        expect(errorDescription, equals(testErrorDescription));
+      });
+      SystemServicesFlutterApiImpl().onCameraError(testErrorDescription);
+    });
   });
 }
diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart
index 55f2c5e..3f0e9c2 100644
--- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart
+++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart
@@ -356,3 +356,120 @@
     }
   }
 }
+
+class _TestPreviewHostApiCodec extends StandardMessageCodec {
+  const _TestPreviewHostApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is ResolutionInfo) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else if (value is ResolutionInfo) {
+      buffer.putUint8(129);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return ResolutionInfo.decode(readValue(buffer)!);
+
+      case 129:
+        return ResolutionInfo.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+abstract class TestPreviewHostApi {
+  static const MessageCodec<Object?> codec = _TestPreviewHostApiCodec();
+
+  void create(int identifier, int? rotation, ResolutionInfo? targetResolution);
+  int setSurfaceProvider(int identifier);
+  void releaseFlutterSurfaceTexture();
+  ResolutionInfo getResolutionInfo(int identifier);
+  static void setup(TestPreviewHostApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.PreviewHostApi.create', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.PreviewHostApi.create was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final int? arg_identifier = (args[0] as int?);
+          assert(arg_identifier != null,
+              'Argument for dev.flutter.pigeon.PreviewHostApi.create was null, expected non-null int.');
+          final int? arg_rotation = (args[1] as int?);
+          final ResolutionInfo? arg_targetResolution =
+              (args[2] as ResolutionInfo?);
+          api.create(arg_identifier!, arg_rotation, arg_targetResolution);
+          return <Object?, Object?>{};
+        });
+      }
+    }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final int? arg_identifier = (args[0] as int?);
+          assert(arg_identifier != null,
+              'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider was null, expected non-null int.');
+          final int output = api.setSurfaceProvider(arg_identifier!);
+          return <Object?, Object?>{'result': output};
+        });
+      }
+    }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture',
+          codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          // ignore message
+          api.releaseFlutterSurfaceTexture();
+          return <Object?, Object?>{};
+        });
+      }
+    }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final int? arg_identifier = (args[0] as int?);
+          assert(arg_identifier != null,
+              'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo was null, expected non-null int.');
+          final ResolutionInfo output = api.getResolutionInfo(arg_identifier!);
+          return <Object?, Object?>{'result': output};
+        });
+      }
+    }
+  }
+}