From 61d6c08130976d937009c038bdb921711a5d213a Mon Sep 17 00:00:00 2001 From: Neal Wadhwa Date: Tue, 22 Oct 2019 14:29:14 -0700 Subject: [PATCH] Code for an example Android app that can capture and save dual-pixel data on the Pixel 3. Part of publications: Learning Single Camera Depth Estimation using Dual-Pixels, ICCV 19 Synthetic Depth-of-Field with a Single-Camera Mobile Phone, SIGGRAPH 18 PiperOrigin-RevId: 276144217 --- dual_pixels/PdCapture.iml | 20 + dual_pixels/README.md | 34 +- dual_pixels/app/app.iml | 139 ++++++ dual_pixels/app/build.gradle | 32 ++ dual_pixels/app/proguard-rules.pro | 21 + dual_pixels/app/src/main/AndroidManifest.xml | 24 + .../pdcapture/PdCaptureActivity.java | 447 ++++++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 34 ++ .../res/drawable/ic_launcher_background.xml | 170 +++++++ .../app/src/main/res/layout/activity_main.xml | 23 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/styles.xml | 11 + dual_pixels/build.gradle | 27 ++ dual_pixels/settings.gradle | 2 + 27 files changed, 1003 insertions(+), 1 deletion(-) create mode 100644 dual_pixels/PdCapture.iml create mode 100644 dual_pixels/app/app.iml create mode 100644 dual_pixels/app/build.gradle create mode 100644 dual_pixels/app/proguard-rules.pro create mode 100644 dual_pixels/app/src/main/AndroidManifest.xml create mode 100644 dual_pixels/app/src/main/java/com/googleresearch/pdcapture/PdCaptureActivity.java create mode 100644 dual_pixels/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 dual_pixels/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 dual_pixels/app/src/main/res/layout/activity_main.xml create mode 100644 dual_pixels/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 dual_pixels/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 dual_pixels/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 dual_pixels/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 dual_pixels/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 dual_pixels/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 dual_pixels/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 dual_pixels/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 dual_pixels/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 dual_pixels/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 dual_pixels/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 dual_pixels/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 dual_pixels/app/src/main/res/values/colors.xml create mode 100644 dual_pixels/app/src/main/res/values/strings.xml create mode 100644 dual_pixels/app/src/main/res/values/styles.xml create mode 100644 dual_pixels/build.gradle create mode 100644 dual_pixels/settings.gradle diff --git a/dual_pixels/PdCapture.iml b/dual_pixels/PdCapture.iml new file mode 100644 index 000000000000..9c7bcaa9830d --- /dev/null +++ b/dual_pixels/PdCapture.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dual_pixels/README.md b/dual_pixels/README.md index 6168c7755e55..099cbd6f7132 100644 --- a/dual_pixels/README.md +++ b/dual_pixels/README.md @@ -18,7 +18,39 @@ Coming soon! ## Android App to Capture Dual-pixel Data -Coming soon! +The app has been tested on the Google Pixel 3 and Pixel 4. + +### Installation instructions: + +1. Download [Android Studio](https://developer.android.com/studio). When you install it, make sure to also install the Android SDK API 29. +2. Click "Open an existing Android Studio project". Select the "dual_pixels" directory. +3. There will be a popup with title "Gradle Sync" complaining about a missing file called gradle-wrapper.properties. Click ok to recreate the Gradle wrapper. +4. Plug in your Pixel smartphone. You'll need to enable USB debugging. See +https://developer.android.com/studio/debug/dev-options for further instructions. +5. Go to the "Run" menu at the top and click "Run 'app'" to compile and install the app. + +Dual-pixel data will be saved in the directory: +``` +/sdcard/Android/data/com.google.reseach.pdcapture/files +``` + +### Information about saved data + +The images are captured with a white level of 1023 (10 bits), but the app +linearly scales them to have white level 65535. That is, the pixels are +initially in the range \[0, 1023\], but are scaled to the range \[0, 65535\]. +These images are in linear space. That is, they have not been gamma-encoded. The +black level is typically around 64 before scaling and 1024 after scaling. The +exact black level can be obtained via [SENSOR_DYNAMIC_BLACK_LEVEL](https://developer.android.com/reference/android/hardware/camera2/CaptureResult.html#SENSOR_DYNAMIC_BLACK_LEVEL) in the CaptureResult. + +This app only saves the dual-pixel images. It is also possible to save files in +ImageFormat.RAW10 and metadata captured at the same time. Other image stream +combinations may not work. + +This app will not work on non-Google phones and will not work on any phone +released by Google prior to the Pixel 3. It should work with the 3a and 4 +running Android 10, but is not guaranteed to work on any Pixel phones after that +point. ## Related Work diff --git a/dual_pixels/app/app.iml b/dual_pixels/app/app.iml new file mode 100644 index 000000000000..046053386ccf --- /dev/null +++ b/dual_pixels/app/app.iml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dual_pixels/app/build.gradle b/dual_pixels/app/build.gradle new file mode 100644 index 000000000000..a93ee14c8d97 --- /dev/null +++ b/dual_pixels/app/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "com.googlereseach.pdcapture" + minSdkVersion 29 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } + } + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/dual_pixels/app/proguard-rules.pro b/dual_pixels/app/proguard-rules.pro new file mode 100644 index 000000000000..f1b424510da5 --- /dev/null +++ b/dual_pixels/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/dual_pixels/app/src/main/AndroidManifest.xml b/dual_pixels/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..09f9f1149dc3 --- /dev/null +++ b/dual_pixels/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/dual_pixels/app/src/main/java/com/googleresearch/pdcapture/PdCaptureActivity.java b/dual_pixels/app/src/main/java/com/googleresearch/pdcapture/PdCaptureActivity.java new file mode 100644 index 000000000000..170940d0743f --- /dev/null +++ b/dual_pixels/app/src/main/java/com/googleresearch/pdcapture/PdCaptureActivity.java @@ -0,0 +1,447 @@ +package com.googleresearch.pdcapture; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.Image; +import android.media.ImageReader; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.util.Size; +import android.view.Gravity; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.ShortBuffer; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.TimeZone; + +/** An activity to capture and save Dual-Pixel data. */ +public class PdCaptureActivity extends Activity { + private static final String TAG = "PdCaptureApp"; + // These are the dimensions of the dual-pixel data. The sensor has 2016x1512 2x2 Bayer quads + // (https://en.wikipedia.org/wiki/Bayer_filter). Each 2x2 quad consists of one pixel that records + // red light, one that records blue light and two pixels that record green light. When demoasiced, + // these pixels can produce a 4032x3024 RGB image. The green pixels are also split in half and + // binned such that the four left and right green half-pixels in adjacent Bayer quads are averaged + // together. This results in dual-pixel data that is one quarter the size of the RGB image + // vertically and one half the size horizontally. The data entirely comes from the green channel. + private static final int DP_WIDTH = 2016; + private static final int DP_HEIGHT = 756; + private static final int DP_CHANNELS = 2; + /** + * Unexposed ImageFormat corresponding to dual-pixel data. This is a hidden member of the enum + * {@link android.graphics.ImageFormat}. + */ + private static final int IMAGE_FORMAT_DP = 0x1002; + + private static final int IMAGE_READER_DP_WIDTH = DP_WIDTH * DP_CHANNELS; + private static final int IMAGE_READER_DP_HEIGHT = DP_HEIGHT; + + /** The white level of dual-pixel data as recorded by the camera. */ + private static final int DP_WHITE_LEVEL = 1023; + + /** The white level we will save dual-pixel data with. */ + private static final int TARGET_WHITE_LEVEL = 65535; + + // User interface. + private Button captureDpButton; + private TextureView viewfinderTextureView; + + // Camera controller objects. + private CameraDevice cameraDevice; + private CameraCaptureSession cameraCaptureSession; + private HandlerThread cameraThread; + private Handler cameraHandler; + private ImageReader imageReader; + private Size previewDimensions; + private File outputDirectory; + + // Permission + private static int CAMERA_PERMISSION_REQUEST_CODE = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + viewfinderTextureView = findViewById(R.id.texture); + viewfinderTextureView.setSurfaceTextureListener(surfaceTextureListener); + captureDpButton = findViewById(R.id.captureDpButton); + // We don't use lambda expressions to avoid requiring Java8. + captureDpButton.setOnClickListener( + new View.OnClickListener() { + public void onClick(View v) { + captureDpData(); + } + }); + outputDirectory = getApplicationContext().getExternalFilesDir(""); + } + + @Override + public void onPause() { + cameraThread.quitSafely(); + try { + cameraThread.join(); + cameraThread = null; + cameraHandler = null; + } catch (InterruptedException e) { + Log.e(TAG, "Unable to stop camera thread: ", e); + } + closeCamera(); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + cameraThread = new HandlerThread("CameraThread"); + cameraThread.start(); + cameraHandler = new Handler(cameraThread.getLooper()); + if (viewfinderTextureView.isAvailable()) { + openCamera(); + } else { + viewfinderTextureView.setSurfaceTextureListener(surfaceTextureListener); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { + if (grantResults.length != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + AlertDialog alertDialog = new AlertDialog.Builder(PdCaptureActivity.this).create(); + alertDialog.setTitle("Camera Permission"); + alertDialog.setMessage("Camera Permission is not granted"); + alertDialog.setButton( + AlertDialog.BUTTON_NEUTRAL, + "OK", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + alertDialog.show(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + /** Creates the camera device and the image reader. */ + private void openCamera() { + CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE); + try { + String cameraId = getFirstRearCameraId(manager); + CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + StreamConfigurationMap streamConfigMap = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + previewDimensions = getLargestByArea(streamConfigMap.getOutputSizes(SurfaceTexture.class)); + Log.d(TAG, "Preview dimensions: " + previewDimensions); + if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE); + return; + } + manager.openCamera(cameraId, stateCallback, null); + createImageReader(); + } catch (CameraAccessException e) { + Log.e(TAG, "Error opening camera: ", e); + } catch (NoSuchElementException e) { + Log.e(TAG, "Error opening camera: ", e); + } + } + + /** + * Gets the ID of the first rear camera. + * + * @throws NoSuchElementException if no rear cameras are found. + */ + private String getFirstRearCameraId(CameraManager cameraManager) + throws CameraAccessException, NoSuchElementException { + String[] cameraIdList = cameraManager.getCameraIdList(); + for (String id : cameraIdList) { + if (cameraManager.getCameraCharacteristics(id).get(CameraCharacteristics.LENS_FACING) + == CameraCharacteristics.LENS_FACING_BACK) { + return id; + } + } + + throw new NoSuchElementException("Couldn't find a rear facing camera."); + } + + /** + * Starts the camera's preview. Configures both a viewfinder stream and a dual-pixel stream, but + * only requests captures from the viewfinder stream. + */ + private void startPreview() { + try { + SurfaceTexture texture = viewfinderTextureView.getSurfaceTexture(); + texture.setDefaultBufferSize(previewDimensions.getWidth(), previewDimensions.getHeight()); + // The image stream is in landscape orientation, but we force the texture to be in portrait + // orientation. Therefore, we reverse height and width here. + adjustAspectRatio(previewDimensions.getHeight(), previewDimensions.getWidth()); + final Surface viewfinderSurface = new Surface(texture); + List outputSurfaces = new ArrayList(2); + outputSurfaces.add(imageReader.getSurface()); + outputSurfaces.add(viewfinderSurface); + cameraDevice.createCaptureSession( + outputSurfaces, + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + Log.d(TAG, "Entering CameraCaptureSession.StateCallback.onConfigured()"); + if (null == cameraDevice) { + Log.e(TAG, "cameraDevice is null when attempting to start repeating request."); + return; + } + cameraCaptureSession = session; + try { + CaptureRequest.Builder captureRequestBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + captureRequestBuilder.addTarget(viewfinderSurface); + captureRequestBuilder.set( + CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), null, cameraHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "Error starting repeating request: ", e); + } + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + Log.e(TAG, "Failure while opening camera."); + } + }, + null); + } catch (CameraAccessException e) { + Log.e(TAG, "Error starting preview: ", e); + } + } + + private void closeCamera() { + if (null != cameraDevice) { + cameraDevice.close(); + cameraDevice = null; + } + if (null != imageReader) { + imageReader.close(); + imageReader = null; + } + } + + /** Inserts a request to capture dual-pixel data into the queue of capture requests. */ + private void captureDpData() { + try { + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(imageReader.getSurface()); + captureBuilder.addTarget(new Surface(viewfinderTextureView.getSurfaceTexture())); + captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + cameraCaptureSession.capture(captureBuilder.build(), null, cameraHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "CameraAccessException: ", e); + } + } + + /** Configures an ImageReader for dual-pixel data. */ + private void createImageReader() { + imageReader = + ImageReader.newInstance( + IMAGE_READER_DP_WIDTH, IMAGE_READER_DP_HEIGHT, IMAGE_FORMAT_DP, /*maxImages=*/ 1); + imageReader.setOnImageAvailableListener( + new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + Image image = null; + try { + image = reader.acquireLatestImage(); + Log.d(TAG, "Acquired image with timestamp: " + image.getTimestamp()); + ShortBuffer buffer = image.getPlanes()[0].getBuffer().asShortBuffer(); + short[] interleavedDpData = new short[buffer.capacity()]; + buffer.get(interleavedDpData); + + DpData dpData = + deinterleaveAndScaleWhiteLevel( + interleavedDpData, DP_WHITE_LEVEL, TARGET_WHITE_LEVEL); + + // Saves images with filenames based on the current time. + String rootName = getCurrentTimeStr(); + Toast toast = + Toast.makeText(PdCaptureActivity.this, "Saved:" + rootName, Toast.LENGTH_SHORT); + toast.setGravity(Gravity.TOP, 0, 0); + toast.show(); + File outputLeft = new File(outputDirectory, rootName + "_left.pgm"); + savePgm16(outputLeft.getAbsolutePath(), dpData.leftView, DP_WIDTH, DP_HEIGHT); + File outputRight = new File(outputDirectory, rootName + "_right.pgm"); + savePgm16(outputRight.getAbsolutePath(), dpData.rightView, DP_WIDTH, DP_HEIGHT); + } finally { + if (image != null) { + image.close(); + } + } + } + }, + cameraHandler); + } + + /** Holds data corresponding to the left and right dual-pixel views. */ + private class DpData { + // This data spans the range [0, 65535]. The fact that short has a range of [-32768, 32767] + // doesn't matter since after scaling, we only deal with the byte representation of the data. + public short[] leftView; + public short[] rightView; + } + + /** Deinterleaves dual-pixel data into two buffers representing the left and right views. */ + private DpData deinterleaveAndScaleWhiteLevel( + short[] interleavedDpData, int inputWhiteLevel, int outputWhiteLevel) { + DpData dpData = new DpData(); + final int viewSize = interleavedDpData.length / 2; + dpData.leftView = new short[viewSize]; + dpData.rightView = new short[viewSize]; + final float multiplier = (float) outputWhiteLevel / inputWhiteLevel; + for (int i = 0; i < viewSize; ++i) { + dpData.leftView[i] = (short) (interleavedDpData[2 * i] * multiplier); + dpData.rightView[i] = (short) (interleavedDpData[2 * i + 1] * multiplier); + } + return dpData; + } + + /** + * Converts a short array to a byte array by turning every short into its big-endian two byte + * representation. + */ + private byte[] shortArrayToByteArray(short[] array) { + byte[] bytes = new byte[array.length * 2]; + for (int i = 0; i < array.length; ++i) { + bytes[2 * i] = (byte) ((array[i] >> 8) & 0xff); + bytes[2 * i + 1] = (byte) (array[i] & 0xff); + } + return bytes; + } + + /** Saves a 16 bit image as a PGM file. */ + private void savePgm16(String filename, short[] imageData, int width, int height) { + String header = String.format("P5 %d %d %d ", width, height, TARGET_WHITE_LEVEL); + OutputStream output = null; + try { + output = new FileOutputStream(filename); + if (null != output) { + output.write(header.getBytes()); + output.write(shortArrayToByteArray(imageData)); + output.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** Returns current date time as a string. */ + private String getCurrentTimeStr() { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS"); + simpleDateFormat.setTimeZone(TimeZone.getDefault()); + return simpleDateFormat.format(System.currentTimeMillis()); + } + + /** + * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a {@link + * TextureView}. + */ + private final TextureView.SurfaceTextureListener surfaceTextureListener = + new TextureView.SurfaceTextureListener() { + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) { + Log.d(TAG, "Surface texture available."); + openCamera(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {} + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) { + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture texture) {} + }; + + private final CameraDevice.StateCallback stateCallback = + new CameraDevice.StateCallback() { + @Override + public void onOpened(CameraDevice camera) { + Log.d(TAG, "Camera has been opened."); + cameraDevice = camera; + startPreview(); + } + + @Override + public void onDisconnected(CameraDevice camera) { + Log.e(TAG, "Camera has been disconnected."); + cameraDevice.close(); + } + + @Override + public void onError(CameraDevice camera, int error) { + cameraDevice.close(); + cameraDevice = null; + } + }; + + /** Get the largest element of sizes sorted by area. */ + private Size getLargestByArea(Size[] sizes) { + assert sizes.length != 0; + int largestArea = -1; + Size largestSize = sizes[0]; + for (Size size : sizes) { + final int area = size.getWidth() * size.getHeight(); + if (area > largestArea) { + largestArea = area; + largestSize = size; + } + } + return largestSize; + } + + /** Adjusts the aspect ratio of viewfinderTextureView to match that of the viewfinder stream. */ + private void adjustAspectRatio(int desiredWidth, int desiredHeight) { + int width = viewfinderTextureView.getWidth(); + int height = viewfinderTextureView.getHeight(); + int newWidth = width; + int newHeight = height; + if (width * desiredHeight > height * desiredWidth) { + newWidth = newHeight * desiredWidth / desiredHeight; + } else { + newHeight = newWidth * desiredHeight / desiredWidth; + } + Matrix transform = new Matrix(); + viewfinderTextureView.getTransform(transform); + transform.setScale((float) newWidth / width, (float) newHeight / height); + viewfinderTextureView.setTransform(transform); + } +} diff --git a/dual_pixels/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/dual_pixels/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000000..1f6bb290603d --- /dev/null +++ b/dual_pixels/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/dual_pixels/app/src/main/res/drawable/ic_launcher_background.xml b/dual_pixels/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..0d025f9bf6b6 --- /dev/null +++ b/dual_pixels/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dual_pixels/app/src/main/res/layout/activity_main.xml b/dual_pixels/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000000..bafefd609132 --- /dev/null +++ b/dual_pixels/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,23 @@ + + + +