diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..4a064069 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "com.wonderkiln.camerakit.demo" + minSdkVersion 21 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:25.0.1' + compile project(':camerakit') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..0b5cf9d3 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/dylanmcintyre/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# 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/app/src/androidTest/java/com/wonderkiln/camerakit/demo/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/wonderkiln/camerakit/demo/ExampleInstrumentedTest.java new file mode 100644 index 00000000..c04c8841 --- /dev/null +++ b/app/src/androidTest/java/com/wonderkiln/camerakit/demo/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.wonderkiln.camerakit.demo; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.wonderkiln.camerakit.demo", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c61fb618 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/wonderkiln/camerakit/demo/MainActivity.java b/app/src/main/java/com/wonderkiln/camerakit/demo/MainActivity.java new file mode 100644 index 00000000..5361c9c9 --- /dev/null +++ b/app/src/main/java/com/wonderkiln/camerakit/demo/MainActivity.java @@ -0,0 +1,14 @@ +package com.wonderkiln.camerakit.demo; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..d705dc74 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..cde69bcc Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..9a078e3e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..c133a0cb Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..efc028a6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..bfa42f0e Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..3af2608a Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..324e72cd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..9bec2e62 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..aee44e13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..34947cd6 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..3ab3e9cb --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..18d9c2f1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + CameraKit-Android + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..5885930d --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/com/wonderkiln/camerakit/demo/ExampleUnitTest.java b/app/src/test/java/com/wonderkiln/camerakit/demo/ExampleUnitTest.java new file mode 100644 index 00000000..76bffabc --- /dev/null +++ b/app/src/test/java/com/wonderkiln/camerakit/demo/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.wonderkiln.camerakit.demo; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..b1557280 --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.0-beta1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/camerakit/.gitignore b/camerakit/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/camerakit/.gitignore @@ -0,0 +1 @@ +/build diff --git a/camerakit/build.gradle b/camerakit/build.gradle new file mode 100644 index 00000000..af4beef5 --- /dev/null +++ b/camerakit/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + +} diff --git a/camerakit/proguard-rules.pro b/camerakit/proguard-rules.pro new file mode 100644 index 00000000..0b5cf9d3 --- /dev/null +++ b/camerakit/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/dylanmcintyre/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# 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/camerakit/src/androidTest/java/com/wonderkiln/camerakit/ExampleInstrumentedTest.java b/camerakit/src/androidTest/java/com/wonderkiln/camerakit/ExampleInstrumentedTest.java new file mode 100644 index 00000000..13d96fbd --- /dev/null +++ b/camerakit/src/androidTest/java/com/wonderkiln/camerakit/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.wonderkiln.camerakit; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.wonderkiln.camerakit.test", appContext.getPackageName()); + } +} diff --git a/camerakit/src/main/AndroidManifest.xml b/camerakit/src/main/AndroidManifest.xml new file mode 100644 index 00000000..631fe928 --- /dev/null +++ b/camerakit/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/AspectRatio.java b/camerakit/src/main/java/com/wonderkiln/camerakit/AspectRatio.java new file mode 100644 index 00000000..dc64b4d1 --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/AspectRatio.java @@ -0,0 +1,170 @@ +package com.wonderkiln.camerakit; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.v4.util.SparseArrayCompat; + +public class AspectRatio implements Comparable, Parcelable { + + private final static SparseArrayCompat> sCache + = new SparseArrayCompat<>(16); + + private final int mX; + private final int mY; + + /** + * Returns an instance of {@link AspectRatio} specified by {@code x} and {@code y} values. + * The values {@code x} and {@code} will be reduced by their greatest common divider. + * + * @param x The width + * @param y The height + * @return An instance of {@link AspectRatio} + */ + public static AspectRatio of(int x, int y) { + int gcd = gcd(x, y); + x /= gcd; + y /= gcd; + SparseArrayCompat arrayX = sCache.get(x); + if (arrayX == null) { + AspectRatio ratio = new AspectRatio(x, y); + arrayX = new SparseArrayCompat<>(); + arrayX.put(y, ratio); + sCache.put(x, arrayX); + return ratio; + } else { + AspectRatio ratio = arrayX.get(y); + if (ratio == null) { + ratio = new AspectRatio(x, y); + arrayX.put(y, ratio); + } + return ratio; + } + } + + /** + * Parse an {@link AspectRatio} from a {@link String} formatted like "4:3". + * + * @param s The string representation of the aspect ratio + * @return The aspect ratio + * @throws IllegalArgumentException when the format is incorrect. + */ + public static AspectRatio parse(String s) { + int position = s.indexOf(':'); + if (position == -1) { + throw new IllegalArgumentException("Malformed aspect ratio: " + s); + } + try { + int x = Integer.parseInt(s.substring(0, position)); + int y = Integer.parseInt(s.substring(position + 1)); + return AspectRatio.of(x, y); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Malformed aspect ratio: " + s, e); + } + } + + private AspectRatio(int x, int y) { + mX = x; + mY = y; + } + + public int getX() { + return mX; + } + + public int getY() { + return mY; + } + + public boolean matches(Size size) { + int gcd = gcd(size.getWidth(), size.getHeight()); + int x = size.getWidth() / gcd; + int y = size.getHeight() / gcd; + return mX == x && mY == y; + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (this == o) { + return true; + } + if (o instanceof AspectRatio) { + AspectRatio ratio = (AspectRatio) o; + return mX == ratio.mX && mY == ratio.mY; + } + return false; + } + + @Override + public String toString() { + return mX + ":" + mY; + } + + public float toFloat() { + return (float) mX / mY; + } + + @Override + public int hashCode() { + // assuming most sizes are <2^16, doing a rotate will give us perfect hashing + return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2))); + } + + @Override + public int compareTo(@NonNull AspectRatio another) { + if (equals(another)) { + return 0; + } else if (toFloat() - another.toFloat() > 0) { + return 1; + } + return -1; + } + + /** + * @return The inverse of this {@link AspectRatio}. + */ + public AspectRatio inverse() { + //noinspection SuspiciousNameCombination + return AspectRatio.of(mY, mX); + } + + private static int gcd(int a, int b) { + while (b != 0) { + int c = b; + b = a % b; + a = c; + } + return a; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mX); + dest.writeInt(mY); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + + @Override + public AspectRatio createFromParcel(Parcel source) { + int x = source.readInt(); + int y = source.readInt(); + return AspectRatio.of(x, y); + } + + @Override + public AspectRatio[] newArray(int size) { + return new AspectRatio[size]; + } + }; + +} \ No newline at end of file diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/Camera2.java b/camerakit/src/main/java/com/wonderkiln/camerakit/Camera2.java new file mode 100644 index 00000000..cb41faed --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/Camera2.java @@ -0,0 +1,539 @@ +package com.wonderkiln.camerakit; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.ImageFormat; +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.CaptureRequest; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaRecorder; +import android.os.Handler; +import android.os.HandlerThread; +import android.support.annotation.NonNull; +import android.util.Log; +import android.util.SparseIntArray; +import android.view.Surface; +import android.view.View; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static android.content.ContentValues.TAG; + +@TargetApi(21) +public class Camera2 extends CameraViewImpl { + + private static final SparseIntArray INTERNAL_FACINGS = new SparseIntArray(); + + static { + INTERNAL_FACINGS.put(Constants.FACING_BACK, CameraCharacteristics.LENS_FACING_BACK); + INTERNAL_FACINGS.put(Constants.FACING_FRONT, CameraCharacteristics.LENS_FACING_FRONT); + } + + private CameraManager mCameraManager; + private CameraDevice mCamera; + private CameraCaptureSession mCaptureSession; + private CaptureRequest.Builder mPreviewRequestBuilder; + private ImageReader mImageReader; + private MediaRecorder mMediaRecorder; + + private Semaphore mCameraOpenCloseLock; + + private int mFacing; + private int mFlash; + private int mDisplayOrientation; + private String mCameraId; + private CameraCharacteristics mCameraCharacteristics; + + private SortedSet mPreviewSizes; + private SortedSet mCaptureSizes; + + private HandlerThread mBackgroundThread; + private Handler mBackgroundHandler; + + Camera2(Context context, CameraListener cameraListener, PreviewImpl preview) { + super(cameraListener, preview); + mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + mPreview.setCallback(new PreviewImpl.Callback() { + @Override + public void onSurfaceChanged() { + startCaptureSession(); + } + }); + + mCameraOpenCloseLock = new Semaphore(1); + + mPreviewSizes = new TreeSet<>(); + mCaptureSizes = new TreeSet<>(); + } + + @Override + View getView() { + return mPreview.getView(); + } + + @Override + void start() { + if (chooseCameraIdByFacing()) { + startBackgroundThread(); + collectCameraInfo(); + prepareImageReader(); + startOpeningCamera(); + } + } + + @Override + void stop() { + try { + mCameraOpenCloseLock.acquire(); + if (mCaptureSession != null) { + mCaptureSession.close(); + mCaptureSession = null; + } + if (mCamera != null) { + mCamera.close(); + mCamera = null; + } + if (mImageReader != null) { + mImageReader.close(); + mImageReader = null; + } + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while trying to lock camera closing.", e); + } finally { + mCameraOpenCloseLock.release(); + + if (mBackgroundThread != null) { + stopBackgroundThread(); + } + } + + } + + @Override + boolean isCameraOpened() { + return mCamera != null; + } + + @Override + void setFacing(int facing) { + int internalFacing = INTERNAL_FACINGS.get(facing); + if (mFacing == internalFacing) return; + this.mFacing = internalFacing; + if (isCameraOpened()) { + stop(); + start(); + } + } + + @Override + int getFacing() { + return mFacing; + } + + @Override + void setFlash(int flash) { + if (mFlash == flash) return; + int fallback = flash; + mFlash = flash; + if (mPreviewRequestBuilder != null) { + updateFlash(mPreviewRequestBuilder); + if (mCaptureSession != null) { + try { + mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + mFlash = fallback; + } + } + } + } + + @Override + int getFlash() { + return mFlash; + } + + @Override + void capturePicture() { + if (mFacing == Constants.FACING_BACK) { + lockFocus(); + } else { + captureStillPicture(); + } + } + + @Override + void captureStill() { + + } + + @Override + void startVideo() { + if (mCamera == null) { + return; + } + } + + @Override + void endVideo() { + + } + + @Override + void setDisplayOrientation(int displayOrientation) { + mDisplayOrientation = displayOrientation; + mPreview.setDisplayOrientation(mDisplayOrientation); + } + + void updateFlash(CaptureRequest.Builder builder) { + switch (mFlash) { + case Constants.FLASH_OFF: + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case Constants.FLASH_ON: + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case Constants.FLASH_AUTO: + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + } + } + + private boolean chooseCameraIdByFacing() { + try { + int internalFacing = mFacing; + final String[] ids = mCameraManager.getCameraIdList(); + if (ids.length == 0) { // No camera + throw new RuntimeException("No camera available."); + } + for (String id : ids) { + CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id); + Integer level = characteristics.get( + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + if (level == null || + level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { + continue; + } + Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING); + if (internal == null) { + throw new NullPointerException("Unexpected state: LENS_FACING null"); + } + if (internal == internalFacing) { + mCameraId = id; + mCameraCharacteristics = characteristics; + return true; + } + } + // Not found + mCameraId = ids[0]; + mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId); + Integer level = mCameraCharacteristics.get( + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + if (level == null || + level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { + return false; + } + Integer internal = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING); + if (internal == null) { + throw new NullPointerException("Unexpected state: LENS_FACING null"); + } + for (int i = 0, count = INTERNAL_FACINGS.size(); i < count; i++) { + if (INTERNAL_FACINGS.valueAt(i) == internal) { + mFacing = INTERNAL_FACINGS.keyAt(i); + return true; + } + } + // The operation can reach here when the only camera device is an external one. + // We treat it as facing back. + mFacing = Constants.FACING_BACK; + return true; + } catch (CameraAccessException e) { + throw new RuntimeException("Failed to get a list of camera devices", e); + } + } + + private void collectCameraInfo() { + StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + if (map == null) { + throw new IllegalStateException("Failed to get configuration map: " + mCameraId); + } + + mPreviewSizes.clear(); + for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) { + mPreviewSizes.add(new Size(size.getWidth(), size.getHeight())); + } + + mCaptureSizes.clear(); + for (android.util.Size size : map.getOutputSizes(ImageFormat.JPEG)) { + mCaptureSizes.add(new Size(size.getWidth(), size.getHeight())); + } + } + + private Size getOptimalPreviewSize() { + int surfaceLonger, surfaceShorter; + final int surfaceWidth = mPreview.getWidth(); + final int surfaceHeight = mPreview.getHeight(); + if (surfaceWidth < surfaceHeight) { + surfaceLonger = surfaceHeight; + surfaceShorter = surfaceWidth; + } else { + surfaceLonger = surfaceWidth; + surfaceShorter = surfaceHeight; + } + // Pick the smallest of those big enough. + for (Size size : mPreviewSizes) { + if (size.getWidth() >= surfaceLonger && size.getHeight() >= surfaceShorter) { + return size; + } + } + // If no size is big enough, pick the largest one. + return mPreviewSizes.last(); + } + + private void prepareImageReader() { + Size previewSize = getOptimalPreviewSize(); + AspectRatio aspectRatio = AspectRatio.of(previewSize.getWidth(), previewSize.getHeight()); + Size bestSize = findSizeClosestTo(1500000, aspectRatio, mCaptureSizes); + mImageReader = ImageReader.newInstance(bestSize.getWidth(), bestSize.getHeight(), ImageFormat.JPEG, 1); + mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); + } + + private Size findSizeClosestTo(int targetLength, AspectRatio targetAspectRatio, SortedSet sizes) { + int closestDistance = Integer.MAX_VALUE; + Size closestSize = null; + for (Size size : sizes) { + if (targetAspectRatio.matches(size)) { + int length = size.getWidth() * size.getHeight(); + int distance = Math.abs(targetLength - length); + if (closestSize == null) { + closestSize = size; + } else { + if (distance < closestDistance) { + closestSize = size; + closestDistance = length; + } + } + } + } + + return closestSize; + } + + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } + + private void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mBackgroundHandler = null; + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + + @SuppressWarnings("MissingPermission") + private void startOpeningCamera() { + try { + if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Time out waiting to lock camera opening."); + } + mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + throw new RuntimeException("Failed to open camera: " + mCameraId, e); + } catch (InterruptedException e) { + throw new RuntimeException("Time out waiting to lock camera opening."); + } + } + + void startCaptureSession() { + if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) { + return; + } + + Size previewSize = getOptimalPreviewSize(); + mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface surface = mPreview.getSurface(); + try { + mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + mPreviewRequestBuilder.addTarget(surface); + mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), mSessionCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + throw new RuntimeException("Failed to start camera session"); + } catch (IllegalStateException e) { + startOpeningCamera(); + } + } + + private void lockFocus() { + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + try { + mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING); + mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to lock focus.", e); + } + } + + private void captureStillPicture() { + try { + CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureRequestBuilder.addTarget(mImageReader.getSurface()); + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE)); + updateFlash(captureRequestBuilder); + // Calculate JPEG orientation. + @SuppressWarnings("ConstantConditions") + int sensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, + (sensorOrientation + + mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) + + 360) % 360); + // Stop preview and capture a still picture. + mCaptureSession.stopRepeating(); + mCaptureSession.capture(captureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + unlockFocus(); + } + }, mBackgroundHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "Cannot capture a still picture.", e); + } + } + + void unlockFocus() { + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); + try { + mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null); + //updateAutoFocus(); + updateFlash(mPreviewRequestBuilder); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW); + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to restart camera preview.", e); + } + } + + + private final CameraDevice.StateCallback mCameraDeviceCallback = new CameraDevice.StateCallback() { + + @Override + public void onOpened(@NonNull CameraDevice camera) { + mCameraOpenCloseLock.release(); + mCamera = camera; + getCameraListener().onCameraOpened(); + startCaptureSession(); + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + getCameraListener().onCameraClosed(); + } + + @Override + public void onDisconnected(@NonNull CameraDevice camera) { + mCameraOpenCloseLock.release(); + mCamera.close(); + mCamera = null; + } + + @Override + public void onError(@NonNull CameraDevice camera, int error) { + mCameraOpenCloseLock.release(); + Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")"); + mCamera = null; + } + + }; + + private final CameraCaptureSession.StateCallback mSessionCallback = new CameraCaptureSession.StateCallback() { + + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + if (mCamera == null) { + return; + } + mCaptureSession = session; + updateFlash(mPreviewRequestBuilder); + try { + mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to start camera preview.", e); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession session) { + Log.e(TAG, "Failed to configure capture session."); + } + + @Override + public void onClosed(@NonNull CameraCaptureSession session) { + if (mCaptureSession != null && mCaptureSession.equals(session)) { + mCaptureSession = null; + } + } + + }; + + private PictureCaptureCallback mCaptureCallback = new PictureCaptureCallback() { + + @Override + public void onPrecaptureRequired() { + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + setState(STATE_PRECAPTURE); + try { + mCaptureSession.capture(mPreviewRequestBuilder.build(), this, mBackgroundHandler); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to run precapture sequence.", e); + } + } + + @Override + public void onReady() { + captureStillPicture(); + } + + }; + + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { + + @Override + public void onImageAvailable(ImageReader reader) { + try (Image image = reader.acquireNextImage()) { + Image.Plane[] planes = image.getPlanes(); + if (planes.length > 0) { + ByteBuffer buffer = planes[0].getBuffer(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + getCameraListener().onPictureTaken(data); + } + } + } + + }; + +} diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/CameraListener.java b/camerakit/src/main/java/com/wonderkiln/camerakit/CameraListener.java new file mode 100644 index 00000000..5ddbbabe --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/CameraListener.java @@ -0,0 +1,27 @@ +package com.wonderkiln.camerakit; + +import java.io.File; + +public abstract class CameraListener { + + public void onCameraOpened() { + + } + + public void onCameraClosed() { + + } + + public void onPictureTaken(File picture) { + + } + + public void onPictureTaken(byte[] picture) { + + } + + public void onVideoTaken(File video) { + + } + +} \ No newline at end of file diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/CameraView.java b/camerakit/src/main/java/com/wonderkiln/camerakit/CameraView.java new file mode 100644 index 00000000..008e8e18 --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/CameraView.java @@ -0,0 +1,254 @@ +package com.wonderkiln.camerakit; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.hardware.display.DisplayManagerCompat; +import android.support.v4.os.ParcelableCompat; +import android.support.v4.os.ParcelableCompatCreatorCallbacks; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.view.Display; +import android.widget.FrameLayout; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public class CameraView extends FrameLayout { + + public static final int FACING_BACK = Constants.FACING_BACK; + + public static final int FACING_FRONT = Constants.FACING_FRONT; + + @IntDef({FACING_BACK, FACING_FRONT}) + @Retention(RetentionPolicy.SOURCE) + public @interface Facing { + } + + public static final int FLASH_OFF = Constants.FLASH_OFF; + + public static final int FLASH_ON = Constants.FLASH_ON; + + public static final int FLASH_AUTO = Constants.FLASH_AUTO; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({FLASH_OFF, FLASH_ON, FLASH_AUTO}) + public @interface Flash { + } + + public static final int PICTURE_MODE_QUALITY = Constants.PICTURE_MODE_QUALITY; + public static final int PICTURE_MODE_SPEED = Constants.PICTURE_MODE_SPEED; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({PICTURE_MODE_QUALITY, PICTURE_MODE_SPEED}) + public @interface PictureMode { + } + + private int mFacing; + private int mDefaultFacing; + + private int mFlash; + private int mDefaultFlash; + + private int mPictureMode; + + private CameraListener mCameraListener; + private DisplayOrientationDetector mDisplayOrientationDetector; + + private CameraViewImpl mCameraImpl; + + public CameraView(@NonNull Context context) { + super(context, null); + } + + public CameraView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + final PreviewImpl preview = new TextureViewPreview(context, this); + mCameraImpl = new Camera2(context, mCameraListener, preview); + + setFacing(mFacing); + setFlash(mFlash); + + mDisplayOrientationDetector = new DisplayOrientationDetector(context) { + @Override + public void onDisplayOrientationChanged(int displayOrientation) { + mCameraImpl.setDisplayOrientation(displayOrientation); + } + }; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mDisplayOrientationDetector.enable( + ViewCompat.isAttachedToWindow(this) + ? DisplayManagerCompat.getInstance(getContext()).getDisplay(Display.DEFAULT_DISPLAY) + : null + ); + } + + @Override + protected void onDetachedFromWindow() { + mDisplayOrientationDetector.disable(); + super.onDetachedFromWindow(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected Parcelable onSaveInstanceState() { + SavedState state = new SavedState(super.onSaveInstanceState()); + state.facing = mFacing; + state.flash = mFlash; + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setFacing(ss.facing); + setFlash(ss.flash); + } + + public void start() { + mCameraImpl.start(); + } + + public void stop() { + if (mCameraImpl != null) { + mCameraImpl.stop(); + } + } + + public void setFacing(@Facing int facing) { + this.mFacing = facing; + if (mCameraImpl != null) { + mCameraImpl.setFacing(facing); + } + } + + @Facing + public int toggleFacing() { + switch (mFacing) { + case FACING_BACK: + setFacing(FACING_FRONT); + break; + + case FACING_FRONT: + setFacing(FACING_BACK); + break; + } + + return mFacing; + } + + public void setFlash(@Flash int flash) { + this.mFlash = flash; + if (mCameraImpl != null) { + mCameraImpl.setFlash(flash); + } + } + + @Flash + public int toggleFlash() { + switch (mFlash) { + case FLASH_OFF: + setFlash(FLASH_ON); + break; + + case FLASH_ON: + setFlash(FLASH_AUTO); + break; + + case FLASH_AUTO: + setFlash(FLASH_OFF); + break; + } + + return mFlash; + } + + public void setPictureMode(@PictureMode int pictureMode) { + this.mPictureMode = pictureMode; + } + + public void setCameraListener(CameraListener cameraListener) { + this.mCameraListener = cameraListener; + mCameraImpl.setCameraListener(cameraListener); + } + + public void capturePicture() { + switch (mPictureMode) { + case PICTURE_MODE_QUALITY: + mCameraImpl.capturePicture(); + break; + case PICTURE_MODE_SPEED: + mCameraImpl.captureStill(); + break; + } + } + + public void startRecordingVideo() { + + } + + public void stopRecordingVideo() { + + } + + protected static class SavedState extends BaseSavedState { + + @Facing + private int facing; + + @Flash + private int flash; + + @SuppressWarnings("WrongConstant") + public SavedState(Parcel source, ClassLoader loader) { + super(source); + facing = source.readInt(); + flash = source.readInt(); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(facing); + out.writeInt(flash); + } + + public static final Parcelable.Creator CREATOR = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { + + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + + }); + + } + +} diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/CameraViewImpl.java b/camerakit/src/main/java/com/wonderkiln/camerakit/CameraViewImpl.java new file mode 100644 index 00000000..790393bc --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/CameraViewImpl.java @@ -0,0 +1,52 @@ +package com.wonderkiln.camerakit; + +import android.view.View; + +abstract class CameraViewImpl { + + protected CameraListener mCameraListener; + + protected final PreviewImpl mPreview; + + CameraViewImpl(CameraListener callback, PreviewImpl preview) { + mCameraListener = callback; + mPreview = preview; + } + + View getView() { + return mPreview.getView(); + } + + abstract void start(); + + abstract void stop(); + + abstract boolean isCameraOpened(); + + abstract void setFacing(int facing); + + abstract int getFacing(); + + abstract void setFlash(int flash); + + abstract int getFlash(); + + abstract void capturePicture(); + + abstract void captureStill(); + + abstract void startVideo(); + + abstract void endVideo(); + + abstract void setDisplayOrientation(int displayOrientation); + + void setCameraListener(CameraListener cameraListener) { + this.mCameraListener = cameraListener; + } + + protected CameraListener getCameraListener() { + return mCameraListener != null ? mCameraListener : new CameraListener() {}; + } + +} diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/Constants.java b/camerakit/src/main/java/com/wonderkiln/camerakit/Constants.java new file mode 100644 index 00000000..8348ca79 --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/Constants.java @@ -0,0 +1,15 @@ +package com.wonderkiln.camerakit; + +public class Constants { + + public static final int FACING_BACK = 0; + public static final int FACING_FRONT = 1; + + public static final int FLASH_OFF = 0; + public static final int FLASH_ON = 1; + public static final int FLASH_AUTO = 2; + + public static final int PICTURE_MODE_QUALITY = 0; + public static final int PICTURE_MODE_SPEED = 1; + +} diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/DisplayOrientationDetector.java b/camerakit/src/main/java/com/wonderkiln/camerakit/DisplayOrientationDetector.java new file mode 100644 index 00000000..6370c959 --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/DisplayOrientationDetector.java @@ -0,0 +1,69 @@ +package com.wonderkiln.camerakit; + +import android.content.Context; +import android.util.SparseIntArray; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.Surface; + +abstract class DisplayOrientationDetector { + + private final OrientationEventListener mOrientationEventListener; + + static final SparseIntArray DISPLAY_ORIENTATIONS = new SparseIntArray(); + + static { + DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0); + DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90); + DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180); + DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270); + } + + private Display mDisplay; + + private int mLastKnownDisplayOrientation = 0; + + public DisplayOrientationDetector(Context context) { + mOrientationEventListener = new OrientationEventListener(context) { + + private int mLastKnownRotation = -1; + + @Override + public void onOrientationChanged(int orientation) { + if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN || + mDisplay == null) { + return; + } + final int rotation = mDisplay.getRotation(); + if (mLastKnownRotation != rotation) { + mLastKnownRotation = rotation; + dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(rotation)); + } + } + }; + } + + public void enable(Display display) { + mDisplay = display; + mOrientationEventListener.enable(); + // Immediately dispatch the first callback + dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(display.getRotation())); + } + + public void disable() { + mOrientationEventListener.disable(); + mDisplay = null; + } + + public int getLastKnownDisplayOrientation() { + return mLastKnownDisplayOrientation; + } + + void dispatchOnDisplayOrientationChanged(int displayOrientation) { + mLastKnownDisplayOrientation = displayOrientation; + onDisplayOrientationChanged(displayOrientation); + } + + public abstract void onDisplayOrientationChanged(int displayOrientation); + +} \ No newline at end of file diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/PictureCaptureCallback.java b/camerakit/src/main/java/com/wonderkiln/camerakit/PictureCaptureCallback.java new file mode 100644 index 00000000..34e891df --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/PictureCaptureCallback.java @@ -0,0 +1,81 @@ +package com.wonderkiln.camerakit; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.support.annotation.NonNull; + +abstract class PictureCaptureCallback extends CameraCaptureSession.CaptureCallback { + + static final int STATE_PREVIEW = 0; + static final int STATE_LOCKING = 1; + static final int STATE_LOCKED = 2; + static final int STATE_PRECAPTURE = 3; + static final int STATE_WAITING = 4; + static final int STATE_CAPTURING = 5; + + private int mState; + + PictureCaptureCallback() { + } + + void setState(int state) { + mState = state; + } + + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { + process(partialResult); + } + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + process(result); + } + + private void process(@NonNull CaptureResult result) { + switch (mState) { + case STATE_LOCKING: { + Integer af = result.get(CaptureResult.CONTROL_AF_STATE); + if (af == null) { + break; + } + if (af == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + af == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + Integer ae = result.get(CaptureResult.CONTROL_AE_STATE); + if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + setState(STATE_CAPTURING); + onReady(); + } else { + setState(STATE_LOCKED); + onPrecaptureRequired(); + } + } + break; + } + case STATE_PRECAPTURE: { + Integer ae = result.get(CaptureResult.CONTROL_AE_STATE); + if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + ae == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED || + ae == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + setState(STATE_WAITING); + } + break; + } + case STATE_WAITING: { + Integer ae = result.get(CaptureResult.CONTROL_AE_STATE); + if (ae == null || ae != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + setState(STATE_CAPTURING); + onReady(); + } + break; + } + } + } + + public abstract void onReady(); + + public abstract void onPrecaptureRequired(); + +} \ No newline at end of file diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/PreviewImpl.java b/camerakit/src/main/java/com/wonderkiln/camerakit/PreviewImpl.java new file mode 100644 index 00000000..36584d6d --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/PreviewImpl.java @@ -0,0 +1,62 @@ +package com.wonderkiln.camerakit; + +import android.graphics.SurfaceTexture; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.View; + +abstract class PreviewImpl { + + interface Callback { + void onSurfaceChanged(); + } + + private Callback mCallback; + + private int mWidth; + + private int mHeight; + + void setCallback(Callback callback) { + mCallback = callback; + } + + abstract Surface getSurface(); + + abstract View getView(); + + abstract Class getOutputClass(); + + abstract void setDisplayOrientation(int displayOrientation); + + abstract boolean isReady(); + + protected void dispatchSurfaceChanged() { + mCallback.onSurfaceChanged(); + } + + SurfaceHolder getSurfaceHolder() { + return null; + } + + SurfaceTexture getSurfaceTexture() { + return null; + } + + void setBufferSize(int width, int height) { + } + + void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + int getWidth() { + return mWidth; + } + + int getHeight() { + return mHeight; + } + +} diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/Size.java b/camerakit/src/main/java/com/wonderkiln/camerakit/Size.java new file mode 100644 index 00000000..48edc842 --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/Size.java @@ -0,0 +1,54 @@ +package com.wonderkiln.camerakit; + +import android.support.annotation.NonNull; + +public class Size implements Comparable { + + private final int mWidth; + private final int mHeight; + + public Size(int width, int height) { + mWidth = width; + mHeight = height; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (this == o) { + return true; + } + if (o instanceof Size) { + Size size = (Size) o; + return mWidth == size.mWidth && mHeight == size.mHeight; + } + return false; + } + + @Override + public String toString() { + return mWidth + "x" + mHeight; + } + + @Override + public int hashCode() { + // assuming most sizes are <2^16, doing a rotate will give us perfect hashing + return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); + } + + @Override + public int compareTo(@NonNull Size another) { + return mWidth * mHeight - another.mWidth * another.mHeight; + } + +} diff --git a/camerakit/src/main/java/com/wonderkiln/camerakit/TextureViewPreview.java b/camerakit/src/main/java/com/wonderkiln/camerakit/TextureViewPreview.java new file mode 100644 index 00000000..ce62850c --- /dev/null +++ b/camerakit/src/main/java/com/wonderkiln/camerakit/TextureViewPreview.java @@ -0,0 +1,130 @@ +package com.wonderkiln.camerakit; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; + +@TargetApi(14) +public class TextureViewPreview extends PreviewImpl { + + private final TextureView mTextureView; + + private int mDisplayOrientation; + + TextureViewPreview(Context context, ViewGroup parent) { + final View view = View.inflate(context, R.layout.texture_view, parent); + mTextureView = (TextureView) view.findViewById(R.id.texture_view); + mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + setSize(width, height); + configureTransform(); + dispatchSurfaceChanged(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + setSize(width, height); + configureTransform(); + dispatchSurfaceChanged(); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + setSize(0, 0); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + } + }); + } + + @Override + void setCallback(Callback callback) { + super.setCallback(callback); + } + + @Override + Surface getSurface() { + return new Surface(mTextureView.getSurfaceTexture()); + } + + @Override + View getView() { + return mTextureView; + } + + @Override + Class getOutputClass() { + return SurfaceTexture.class; + } + + @Override + void setDisplayOrientation(int displayOrientation) { + mDisplayOrientation = displayOrientation; + configureTransform(); + } + + @Override + boolean isReady() { + return mTextureView.getSurfaceTexture() != null; + } + + @Override + protected void dispatchSurfaceChanged() { + super.dispatchSurfaceChanged(); + } + + @Override + SurfaceTexture getSurfaceTexture() { + return mTextureView.getSurfaceTexture(); + } + + @TargetApi(15) + @Override + void setBufferSize(int width, int height) { + mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height); + } + + void configureTransform() { + Matrix matrix = new Matrix(); + if (mDisplayOrientation % 180 == 90) { + final int width = getWidth(); + final int height = getHeight(); + // Rotate the camera preview when the screen is landscape. + matrix.setPolyToPoly( + new float[]{ + 0.f, 0.f, // top left + width, 0.f, // top right + 0.f, height, // bottom left + width, height, // bottom right + }, 0, + mDisplayOrientation == 90 ? + // Clockwise + new float[]{ + 0.f, height, // top left + 0.f, 0.f, // top right + width, height, // bottom left + width, 0.f, // bottom right + } : // mDisplayOrientation == 270 + // Counter-clockwise + new float[]{ + width, 0.f, // top left + width, height, // top right + 0.f, 0.f, // bottom left + 0.f, height, // bottom right + }, 0, + 4); + } + mTextureView.setTransform(matrix); + } + +} diff --git a/camerakit/src/main/res/layout/texture_view.xml b/camerakit/src/main/res/layout/texture_view.xml new file mode 100644 index 00000000..33a5cdb9 --- /dev/null +++ b/camerakit/src/main/res/layout/texture_view.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/camerakit/src/main/res/values/attrs.xml b/camerakit/src/main/res/values/attrs.xml new file mode 100644 index 00000000..5770a55b --- /dev/null +++ b/camerakit/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/camerakit/src/main/res/values/strings.xml b/camerakit/src/main/res/values/strings.xml new file mode 100644 index 00000000..252d7984 --- /dev/null +++ b/camerakit/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + CameraKit + diff --git a/camerakit/src/test/java/com/wonderkiln/camerakit/ExampleUnitTest.java b/camerakit/src/test/java/com/wonderkiln/camerakit/ExampleUnitTest.java new file mode 100644 index 00000000..d212a96c --- /dev/null +++ b/camerakit/src/test/java/com/wonderkiln/camerakit/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.wonderkiln.camerakit; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..aac7c9b4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..fb1fe53a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jan 17 16:25:25 EST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.2-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..711670b0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':camerakit'