diff --git a/build.gradle b/build.gradle index 7deafe884..b02aa6d33 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,9 @@ repositories { maven { // material-dialogs url "https://jitpack.io" } + maven { // cwac-camera + url 'https://repo.commonsware.com.s3.amazonaws.com' + } jcenter() mavenLocal() } @@ -72,6 +75,7 @@ dependencies { exclude group: 'com.android.support', module: 'support-v4' } compile 'com.madgag.spongycastle:prov:1.51.0.0' + compile 'com.commonsware.cwac:camera:0.6.12' provided 'com.squareup.dagger:dagger-compiler:1.2.2' compile 'org.whispersystems:jobmanager:1.0.2' @@ -124,6 +128,7 @@ dependencyVerification { 'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883', 'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d', 'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a', + 'com.commonsware.cwac:camera:dcc93ddbb2f0393114fa1f31a13fe9e6edfcf5dbe96b22bc4b66c7b15e179054', 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181', 'org.whispersystems:libpastelog:550d33c565380d90f4c671e7b8ed5f3a6da55a9fda468373177106b2eb5220b2', 'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb', diff --git a/res/layout/quick_attachment_drawer.xml b/res/layout/quick_attachment_drawer.xml index 62ec6213a..1bb390759 100644 --- a/res/layout/quick_attachment_drawer.xml +++ b/res/layout/quick_attachment_drawer.xml @@ -1,7 +1,7 @@ - sizes = camera.getParameters().getSupportedPreviewSizes(); - - Collections.sort(sizes, Collections.reverseOrder(new SizeComparator())); - - for (Size size : sizes) { - double ratio = (double)size.width / size.height; - - if (Math.abs(ratio - targetRatio) < minDiff) { - optimalSize = size; - minDiff = Math.abs(ratio - targetRatio); - } - } - - return optimalSize; - } - - private static class SizeComparator implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - int left = lhs.width * lhs.height; - int right = rhs.width * rhs.height; - - if (left < right) return -1; - if (left > right) return 1; - else return 0; - } - } -} diff --git a/src/org/thoughtcrime/securesms/components/camera/CameraView.java b/src/org/thoughtcrime/securesms/components/camera/CameraView.java index cb2d81a18..6be20ebb8 100644 --- a/src/org/thoughtcrime/securesms/components/camera/CameraView.java +++ b/src/org/thoughtcrime/securesms/components/camera/CameraView.java @@ -20,47 +20,44 @@ import android.app.Activity; import android.content.Context; import android.content.pm.ActivityInfo; import android.graphics.Color; -import android.graphics.Rect; import android.hardware.Camera; -import android.hardware.Camera.CameraInfo; -import android.hardware.Camera.Parameters; -import android.hardware.Camera.Size; -import android.os.AsyncTask; +import android.hardware.Camera.PreviewCallback; import android.os.Build; -import android.os.Build.VERSION; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.OrientationEventListener; import android.view.Surface; +import android.view.View; import android.widget.FrameLayout; import java.io.IOException; -import java.util.List; +import java.util.concurrent.CountDownLatch; + +import com.commonsware.cwac.camera.CameraHost; +import com.commonsware.cwac.camera.CameraHost.FailureReason; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.Job; import org.whispersystems.jobqueue.JobParameters; -import org.whispersystems.libaxolotl.util.guava.Optional; @SuppressWarnings("deprecation") public class CameraView extends FrameLayout { private static final String TAG = CameraView.class.getSimpleName(); - private final CameraSurfaceView surface; - private final OnOrientationChange onOrientationChange; - - private @NonNull volatile Optional camera = Optional.absent(); - private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK; - - private boolean started; - private @Nullable CameraViewListener listener; - private int displayOrientation = -1; - private int outputOrientation = -1; + private PreviewStrategy previewStrategy = null; + private Camera.Size previewSize = null; + private volatile Camera camera = null; + private boolean inPreview = false; + private boolean cameraReady = false; + private CameraHost host = null; + private OnOrientationChange onOrientationChange = null; + private int displayOrientation = -1; + private int outputOrientation = -1; + private int cameraId = -1; + private int lastPictureOrientation = -1; public CameraView(Context context) { this(context, null); @@ -74,39 +71,51 @@ public class CameraView extends FrameLayout { super(context, attrs, defStyle); setBackgroundColor(Color.BLACK); - if (isMultiCamera()) cameraId = CameraInfo.CAMERA_FACING_FRONT; - - surface = new CameraSurfaceView(getContext()); onOrientationChange = new OnOrientationChange(context.getApplicationContext()); - addView(surface); + } + + public CameraHost getHost() { + return host; + } + + public void setHost(CameraHost host) { + this.host = host; + + if (host.getDeviceProfile().useTextureView()) { + previewStrategy = new TexturePreviewStrategy(this); + } else { + previewStrategy = new SurfacePreviewStrategy(this); + } + addView(previewStrategy.getWidget()); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void onResume() { - if (started) return; - started = true; Log.w(TAG, "onResume() queued"); - enqueueTask(new SerialAsyncTask() { - @Override - protected @Nullable Camera onRunBackground() { + final CameraHost host = getHost(); + submitTask(new SerializedAsyncTask() { + @Override protected FailureReason onRunBackground() { try { - return Camera.open(cameraId); + cameraId = host.getCameraId(); + if (cameraId >= 0) { + camera = Camera.open(cameraId); + } else { + return FailureReason.NO_CAMERAS_REPORTED; + } } catch (Exception e) { - Log.w(TAG, e); - return null; + return FailureReason.UNKNOWN; } + + return null; } - @Override - protected void onPostMain(@Nullable Camera camera) { - if (camera == null) { - Log.w(TAG, "tried to open camera but got null"); - if (listener != null) listener.onCameraFail(); + @Override protected void onPostMain(FailureReason result) { + if (result != null) { + host.onCameraFail(result); return; } - - CameraView.this.camera = Optional.of(camera); try { + cameraReady = true; if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { onOrientationChange.enable(); } @@ -114,196 +123,227 @@ public class CameraView extends FrameLayout { synchronized (CameraView.this) { CameraView.this.notifyAll(); } - onCameraReady(); + previewCreated(); + initPreview(); requestLayout(); invalidate(); Log.w(TAG, "onResume() completed"); - } catch (RuntimeException e) { - Log.w(TAG, "exception when starting camera preview", e); - onPause(); + } catch (RuntimeException re) { + Log.w(TAG, "exception when starting camera preview", re); + try { + previewDestroyed(); + } catch (RuntimeException re2) { + Log.w(TAG, "also failed to release camera", re2); + } } } }); } public void onPause() { - if (!started) return; - started = false; Log.w(TAG, "onPause() queued"); - - enqueueTask(new SerialAsyncTask() { - private Optional cameraToDestroy; + submitTask(new SerializedAsyncTask() { @Override protected void onPreMain() { - cameraToDestroy = camera; - camera = Optional.absent(); + cameraReady = false; } @Override protected Void onRunBackground() { - if (cameraToDestroy.isPresent()) { - try { - stopPreview(); - cameraToDestroy.get().release(); - Log.w(TAG, "released old camera instance"); - } catch (Exception e) { - Log.w(TAG, e); - } - } + previewDestroyed(); return null; } @Override protected void onPostMain(Void avoid) { onOrientationChange.disable(); + previewSize = null; displayOrientation = -1; outputOrientation = -1; + cameraId = -1; + lastPictureOrientation = -1; Log.w(TAG, "onPause() completed"); } }); } - public boolean isStarted() { - return started; - } + // based on CameraPreview.java from ApiDemos @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0 && camera.isPresent()) { - final Size preferredPreviewSize = CameraUtils.getPreferredPreviewSize(displayOrientation, - getMeasuredWidth(), - getMeasuredHeight(), - camera.get()); - final Parameters parameters = camera.get().getParameters(); - if (preferredPreviewSize != null && !parameters.getPreviewSize().equals(preferredPreviewSize)) { - Log.w(TAG, "setting preview size to " + preferredPreviewSize.width + "x" + preferredPreviewSize.height); - stopPreview(); - parameters.setPreviewSize(preferredPreviewSize.width, preferredPreviewSize.height); - camera.get().setParameters(parameters); - requestLayout(); - startPreview(); + if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0 && camera != null && cameraReady) { + Camera.Size newSize = null; + + try { + if (getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY) { + newSize = getHost().getPreferredPreviewSizeForVideo(getDisplayOrientation(), + getMeasuredWidth(), + getMeasuredHeight(), + camera.getParameters(), + null); + } + if (newSize == null || newSize.width * newSize.height < 65536) { + newSize = getHost().getPreviewSize(getDisplayOrientation(), + getMeasuredWidth(), + getMeasuredHeight(), + camera.getParameters()); + } + } catch (Exception e) { + Log.e(TAG, "Could not work with camera parameters?", e); + // TODO get this out to library clients + } + + if (newSize != null) { + if (previewSize == null) { + previewSize = newSize; + synchronized (this) { notifyAll(); } + } else if (previewSize.width != newSize.width || previewSize.height != newSize.height) { + if (inPreview) { + stopPreview(); + } + + previewSize = newSize; + synchronized (this) { notifyAll(); } + initPreview(); + } } } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); } - @SuppressWarnings("SuspiciousNameCombination") - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - final int width = r - l; - final int height = b - t; - final int previewWidth; - final int previewHeight; + // based on CameraPreview.java from ApiDemos - if (camera.isPresent()) { - final Size previewSize = camera.get().getParameters().getPreviewSize(); - if (displayOrientation == 90 || displayOrientation == 270) { + @SuppressWarnings("SuspiciousNameCombination") @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (getChildCount() > 0) { + final View child = getChildAt(0); + final int width = r - l; + final int height = b - t; + final int previewWidth; + final int previewHeight; + + // handle orientation + + if (previewSize != null && (getDisplayOrientation() == 90 || getDisplayOrientation() == 270)) { previewWidth = previewSize.height; previewHeight = previewSize.width; - } else { + } else if (previewSize != null) { previewWidth = previewSize.width; previewHeight = previewSize.height; + } else { + previewWidth = width; + previewHeight = height; } - } else { - previewWidth = width; - previewHeight = height; - } - if (previewHeight == 0 || previewWidth == 0) { - Log.w(TAG, "skipping layout due to zero-width/height preview size"); - return; - } - Log.w(TAG, "layout " + width + "x" + height + ", target " + previewWidth + "x" + previewHeight); + if (previewHeight == 0 || previewWidth == 0) { + Log.w(TAG, "skipping layout due to zero-width/height preview size"); + return; + } - if (width * previewHeight > height * previewWidth) { - final int scaledChildHeight = previewHeight * width / previewWidth; - surface.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2); - } else { - final int scaledChildWidth = previewWidth * height / previewHeight; - surface.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height); + boolean useFirstStrategy = (width * previewHeight > height * previewWidth); + boolean useFullBleed = getHost().useFullBleedPreview(); + + if ((useFirstStrategy && !useFullBleed) || (!useFirstStrategy && useFullBleed)) { + final int scaledChildWidth = previewWidth * height / previewHeight; + child.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height); + } else { + final int scaledChildHeight = previewHeight * width / previewWidth; + child.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2); + } } } - public void setListener(@Nullable CameraViewListener listener) { - this.listener = listener; + public int getDisplayOrientation() { + return displayOrientation; } - public boolean isMultiCamera() { - return Camera.getNumberOfCameras() > 1; + public void setOneShotPreviewCallback(PreviewCallback callback) { + if (camera != null) camera.setOneShotPreviewCallback(callback); } - public boolean isRearCamera() { - return cameraId == CameraInfo.CAMERA_FACING_BACK; + public @Nullable Camera.Parameters getCameraParameters() { + return camera == null || !cameraReady ? null : camera.getParameters(); } - public void flipCamera() { - if (Camera.getNumberOfCameras() > 1) { - cameraId = cameraId == CameraInfo.CAMERA_FACING_BACK - ? CameraInfo.CAMERA_FACING_FRONT - : CameraInfo.CAMERA_FACING_BACK; - onPause(); - onResume(); - } - } - - @TargetApi(14) - private void onCameraReady() { - if (!camera.isPresent()) return; - - final Parameters parameters = camera.get().getParameters(); - final List focusModes = parameters.getSupportedFocusModes(); - - if (VERSION.SDK_INT >= 14) parameters.setRecordingHint(true); - - if (VERSION.SDK_INT >= 14 && focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { - parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); - } else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { - parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); - } - - camera.get().setParameters(parameters); - - enqueueTask(new PostInitializationTask() { + void previewCreated() { + Log.w(TAG, "previewCreated() queued"); + final CameraHost host = getHost(); + submitTask(new PostInitializationTask() { @Override protected void onPostMain(Void avoid) { - if (camera.isPresent()) { - try { - camera.get().setPreviewDisplay(surface.getHolder()); - requestLayout(); - } catch (Exception e) { - Log.w(TAG, e); + try { + if (camera != null) { + previewStrategy.attach(camera); } + } catch (IOException e) { + host.handleException(e); + } + Log.w(TAG, "previewCreated() completed"); + } + }); + } + + void previewDestroyed() { + try { + if (camera != null) { + previewStopped(); + camera.release(); + } + } finally { + camera = null; + } + } + + private void previewStopped() { + if (inPreview) { + stopPreview(); + } + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void initPreview() { + Log.w(TAG, "initPreview() queued"); + submitTask(new PostInitializationTask() { + @Override protected void onPostMain(Void avoid) { + if (camera != null && cameraReady) { + Camera.Parameters parameters = camera.getParameters(); + + parameters.setPreviewSize(previewSize.width, previewSize.height); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + parameters.setRecordingHint(getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY); + } + + camera.setParameters(getHost().adjustPreviewParameters(parameters)); + startPreview(); + requestLayout(); + invalidate(); + Log.w(TAG, "initPreview() completed"); } } }); } private void startPreview() { - if (camera.isPresent()) { - try { - camera.get().startPreview(); - } catch (Exception e) { - Log.w(TAG, e); - } - } + camera.startPreview(); + inPreview = true; + getHost().autoFocusAvailable(); } private void stopPreview() { - if (camera.isPresent()) { - try { - camera.get().stopPreview(); - } catch (Exception e) { - Log.w(TAG, e); - } - } + camera.startPreview(); + inPreview = false; + getHost().autoFocusUnavailable(); + camera.stopPreview(); } // based on // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) // and http://stackoverflow.com/a/10383164/115145 private void setCameraDisplayOrientation() { - Camera.CameraInfo info = getCameraInfo(); + Camera.CameraInfo info = new Camera.CameraInfo(); int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation(); int degrees = 0; DisplayMetrics dm = new DisplayMetrics(); + Camera.getCameraInfo(cameraId, info); getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm); switch (rotation) { @@ -316,55 +356,69 @@ public class CameraView extends FrameLayout { if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { displayOrientation = (info.orientation + degrees ) % 360; displayOrientation = (360 - displayOrientation) % 360; - } else { + } + else { displayOrientation = (info.orientation - degrees + 360) % 360; } - stopPreview(); - camera.get().setDisplayOrientation(displayOrientation); - startPreview(); + boolean wasInPreview = inPreview; + + if (inPreview) { + stopPreview(); + } + + camera.setDisplayOrientation(displayOrientation); + + if (wasInPreview) { + startPreview(); + } } public int getCameraPictureOrientation() { + Camera.CameraInfo info = new Camera.CameraInfo(); + + Camera.getCameraInfo(cameraId, info); + if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { outputOrientation = getCameraPictureRotation(getActivity().getWindowManager() .getDefaultDisplay() .getOrientation()); - } else if (getCameraInfo().facing == CameraInfo.CAMERA_FACING_FRONT) { + } else if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { outputOrientation = (360 - displayOrientation) % 360; } else { outputOrientation = displayOrientation; } + if (lastPictureOrientation != outputOrientation) { + lastPictureOrientation = outputOrientation; + } return outputOrientation; } - private @NonNull CameraInfo getCameraInfo() { - final CameraInfo info = new Camera.CameraInfo(); - Camera.getCameraInfo(cameraId, info); - return info; - } - - // XXX this sucks - private Activity getActivity() { - return (Activity)getContext(); - } + // based on: + // http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int) public int getCameraPictureRotation(int orientation) { - final CameraInfo info = getCameraInfo(); - final int rotation; + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraId, info); + int rotation; orientation = (orientation + 45) / 90 * 90; if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { rotation = (info.orientation - orientation + 360) % 360; - } else { + } + else { // back-facing camera rotation = (info.orientation + orientation) % 360; } return rotation; } + Activity getActivity() { + return (Activity)getContext(); + } + private class OnOrientationChange extends OrientationEventListener { public OnOrientationChange(Context context) { super(context); @@ -373,18 +427,19 @@ public class CameraView extends FrameLayout { @Override public void onOrientationChanged(int orientation) { - if (camera.isPresent() && orientation != ORIENTATION_UNKNOWN) { + if (camera != null && orientation != ORIENTATION_UNKNOWN) { int newOutputOrientation = getCameraPictureRotation(orientation); if (newOutputOrientation != outputOrientation) { outputOrientation = newOutputOrientation; - Camera.Parameters params = camera.get().getParameters(); + Camera.Parameters params = camera.getParameters(); params.setRotation(outputOrientation); try { - camera.get().setParameters(params); + camera.setParameters(params); + lastPictureOrientation = outputOrientation; } catch (Exception e) { Log.e(TAG, "Exception updating camera parameters in orientation change", e); @@ -394,65 +449,13 @@ public class CameraView extends FrameLayout { } } - public void takePicture(final Rect previewRect) { - if (!camera.isPresent() || camera.get().getParameters() == null) { - Log.w(TAG, "camera not in capture-ready state"); - return; - } - - camera.get().setOneShotPreviewCallback(new Camera.PreviewCallback() { - @Override - public void onPreviewFrame(byte[] data, final Camera camera) { - final int rotation = getCameraPictureOrientation(); - final Size previewSize = camera.getParameters().getPreviewSize(); - final Rect croppingRect = getCroppedRect(previewSize, previewRect, rotation); - - Log.w(TAG, "previewSize: " + previewSize.width + "x" + previewSize.height); - Log.w(TAG, "data bytes: " + data.length); - Log.w(TAG, "previewFormat: " + camera.getParameters().getPreviewFormat()); - Log.w(TAG, "croppingRect: " + croppingRect.toString()); - Log.w(TAG, "rotation: " + rotation); - new CaptureTask(previewSize, rotation, croppingRect).execute(data); - } - }); - } - - private Rect getCroppedRect(Size cameraPreviewSize, Rect visibleRect, int rotation) { - final int previewWidth = cameraPreviewSize.width; - final int previewHeight = cameraPreviewSize.height; - - if (rotation % 180 > 0) rotateRect(visibleRect); - - float scale = (float) previewWidth / visibleRect.width(); - if (visibleRect.height() * scale > previewHeight) { - scale = (float) previewHeight / visibleRect.height(); - } - final float newWidth = visibleRect.width() * scale; - final float newHeight = visibleRect.height() * scale; - final float centerX = (VERSION.SDK_INT < 14) ? previewWidth - newWidth / 2 : previewWidth / 2; - final float centerY = previewHeight / 2; - - visibleRect.set((int) (centerX - newWidth / 2), - (int) (centerY - newHeight / 2), - (int) (centerX + newWidth / 2), - (int) (centerY + newHeight / 2)); - - if (rotation % 180 > 0) rotateRect(visibleRect); - return visibleRect; - } - - @SuppressWarnings("SuspiciousNameCombination") - private void rotateRect(Rect rect) { - rect.set(rect.top, rect.left, rect.bottom, rect.right); - } - - private void enqueueTask(SerialAsyncTask job) { + private void submitTask(SerializedAsyncTask job) { ApplicationContext.getInstance(getContext()).getJobManager().add(job); } - private static abstract class SerialAsyncTask extends Job { + private static abstract class SerializedAsyncTask extends Job { - public SerialAsyncTask() { + public SerializedAsyncTask() { super(JobParameters.newBuilder().withGroupId(CameraView.class.getSimpleName()).create()); } @@ -461,7 +464,7 @@ public class CameraView extends FrameLayout { @Override public final void onRun() { try { onWait(); - Util.runOnMainSync(new Runnable() { + runOnMainSync(new Runnable() { @Override public void run() { onPreMain(); } @@ -469,7 +472,7 @@ public class CameraView extends FrameLayout { final Result result = onRunBackground(); - Util.runOnMainSync(new Runnable() { + runOnMainSync(new Runnable() { @Override public void run() { onPostMain(result); } @@ -485,62 +488,44 @@ public class CameraView extends FrameLayout { @Override public void onCanceled() { } + private void runOnMainSync(final Runnable runnable) { + final CountDownLatch sync = new CountDownLatch(1); + Util.runOnMain(new Runnable() { + @Override public void run() { + try { + runnable.run(); + } finally { + sync.countDown(); + } + } + }); + try { + sync.await(); + } catch (InterruptedException ie) { + throw new AssertionError(ie); + } + } + protected void onWait() throws PreconditionsNotMetException {} protected void onPreMain() {} protected Result onRunBackground() { return null; } protected void onPostMain(Result result) {} } - private abstract class PostInitializationTask extends SerialAsyncTask { + private abstract class PostInitializationTask extends SerializedAsyncTask { @Override protected void onWait() throws PreconditionsNotMetException { synchronized (CameraView.this) { - if (!camera.isPresent()) { + if (!cameraReady) { throw new PreconditionsNotMetException(); } - while (getMeasuredHeight() <= 0 || getMeasuredWidth() <= 0 || !surface.isReady()) { - Log.w(TAG, String.format("waiting. surface ready? %s", surface.isReady())); + while (camera == null || previewSize == null || !previewStrategy.isReady()) { + Log.w(TAG, String.format("waiting. camera? %s previewSize? %s prevewStrategy? %s", + camera != null, previewSize != null, previewStrategy.isReady())); Util.wait(CameraView.this, 0); } } } } - private class CaptureTask extends AsyncTask { - private final Size previewSize; - private final int rotation; - private final Rect croppingRect; - - public CaptureTask(Size previewSize, int rotation, Rect croppingRect) { - this.previewSize = previewSize; - this.rotation = rotation; - this.croppingRect = croppingRect; - } - - @Override - protected byte[] doInBackground(byte[]... params) { - final byte[] data = params[0]; - try { - return BitmapUtil.createFromNV21(data, - previewSize.width, - previewSize.height, - rotation, - croppingRect); - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - @Override - protected void onPostExecute(byte[] imageBytes) { - if (imageBytes != null && listener != null) listener.onImageCapture(imageBytes); - } - } - private static class PreconditionsNotMetException extends Exception {} - - public interface CameraViewListener { - void onImageCapture(@NonNull final byte[] imageBytes); - void onCameraFail(); - } } diff --git a/src/org/thoughtcrime/securesms/components/camera/PreviewStrategy.java b/src/org/thoughtcrime/securesms/components/camera/PreviewStrategy.java new file mode 100644 index 000000000..14ce75f48 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/camera/PreviewStrategy.java @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.components.camera; + +import android.hardware.Camera; +import android.media.MediaRecorder; +import android.view.View; + +import java.io.IOException; + +@SuppressWarnings("deprecation") +public interface PreviewStrategy extends com.commonsware.cwac.camera.PreviewStrategy { + boolean isReady(); +} diff --git a/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java b/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java index d20dbf65b..d62e272d7 100644 --- a/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java +++ b/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java @@ -26,7 +26,7 @@ import com.nineoldandroids.animation.ObjectAnimator; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.InputAwareLayout.InputView; import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout; -import org.thoughtcrime.securesms.components.camera.CameraView.CameraViewListener; +import org.thoughtcrime.securesms.components.camera.QuickCamera.QuickCameraListener; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; @@ -36,7 +36,7 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { private final ViewDragHelper dragHelper; - private CameraView cameraView; + private QuickCamera quickCamera; private int coverViewPosition; private KeyboardAwareLinearLayout container; private View coverView; @@ -74,12 +74,12 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { private void initializeView() { inflate(getContext(), R.layout.quick_attachment_drawer, this); - cameraView = ViewUtil.findById(this, R.id.quick_camera); + quickCamera = (QuickCamera) findViewById(R.id.quick_camera); updateControlsView(); coverViewPosition = getChildCount(); controls.setVisibility(GONE); - cameraView.setVisibility(GONE); + quickCamera.setVisibility(GONE); } public static boolean isDeviceSupported(Context context) { @@ -108,7 +108,7 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { this.rotation = rotation; if (rotationChanged) { if (isShowing()) { - cameraView.onPause(); + quickCamera.onPause(); } updateControlsView(); setDrawerStateAndUpdate(drawerState, true); @@ -123,13 +123,13 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { shutterButton = (ImageButton) controls.findViewById(R.id.shutter_button); swapCameraButton = (ImageButton) controls.findViewById(R.id.swap_camera_button); fullScreenButton = (ImageButton) controls.findViewById(R.id.fullscreen_button); - if (cameraView.isMultiCamera()) { + if (quickCamera.isMultipleCameras()) { swapCameraButton.setVisibility(View.VISIBLE); swapCameraButton.setOnClickListener(new CameraFlipClickListener()); } shutterButton.setOnClickListener(new ShutterClickListener()); fullScreenButton.setOnClickListener(new FullscreenClickListener()); - ViewUtil.swapChildInPlace(this, this.controls, controls, indexOfChild(cameraView) + 1); + ViewUtil.swapChildInPlace(this, this.controls, controls, indexOfChild(quickCamera) + 1); this.controls = controls; } @@ -170,11 +170,11 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { int childLeft = paddingLeft; int childBottom; - if (child == cameraView) { + if (child == quickCamera) { childTop = computeCameraTopPosition(slideOffset); childBottom = childTop + childHeight; - if (cameraView.getMeasuredWidth() < getMeasuredWidth()) - childLeft = (getMeasuredWidth() - cameraView.getMeasuredWidth()) / 2 + paddingLeft; + if (quickCamera.getMeasuredWidth() < getMeasuredWidth()) + childLeft = (getMeasuredWidth() - quickCamera.getMeasuredWidth()) / 2 + paddingLeft; } else if (child == controls) { childBottom = getMeasuredHeight(); } else { @@ -271,14 +271,14 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { ViewCompat.postInvalidateOnAnimation(this); } - if (slideOffset == 0 && cameraView.isStarted()) { - cameraView.onPause(); + if (slideOffset == 0 && quickCamera.isStarted()) { + quickCamera.onPause(); controls.setVisibility(GONE); - cameraView.setVisibility(GONE); - } else if (slideOffset != 0 && !cameraView.isStarted() & !paused) { + quickCamera.setVisibility(GONE); + } else if (slideOffset != 0 && !quickCamera.isStarted() & !paused) { controls.setVisibility(VISIBLE); - cameraView.setVisibility(VISIBLE); - cameraView.onResume(); + quickCamera.setVisibility(VISIBLE); + quickCamera.onResume(); } } @@ -335,10 +335,10 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { public void setListener(AttachmentDrawerListener listener) { this.listener = listener; - if (cameraView != null) cameraView.setListener(listener); + if (quickCamera != null) quickCamera.setQuickCameraListener(listener); } - public interface AttachmentDrawerListener extends CameraViewListener { + public interface AttachmentDrawerListener extends QuickCameraListener { void onAttachmentDrawerStateChanged(DrawerState drawerState); } @@ -391,8 +391,8 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { int slideOffset = getTargetSlideOffset(); dragHelper.captureChildView(coverView, 0); dragHelper.settleCapturedViewAt(coverView.getLeft(), computeCoverTopPosition(slideOffset)); - dragHelper.captureChildView(cameraView, 0); - dragHelper.settleCapturedViewAt(cameraView.getLeft(), computeCameraTopPosition(slideOffset)); + dragHelper.captureChildView(quickCamera, 0); + dragHelper.settleCapturedViewAt(quickCamera.getLeft(), computeCameraTopPosition(slideOffset)); ViewCompat.postInvalidateOnAnimation(QuickAttachmentDrawer.this); } } @@ -455,13 +455,13 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { @SuppressWarnings("ResourceType") private boolean isDragViewUnder(int x, int y) { int[] viewLocation = new int[2]; - cameraView.getLocationOnScreen(viewLocation); + quickCamera.getLocationOnScreen(viewLocation); int[] parentLocation = new int[2]; this.getLocationOnScreen(parentLocation); int screenX = parentLocation[0] + x; int screenY = parentLocation[1] + y; - return screenX >= viewLocation[0] && screenX < viewLocation[0] + cameraView.getWidth() && - screenY >= viewLocation[1] && screenY < viewLocation[1] + cameraView.getHeight(); + return screenX >= viewLocation[0] && screenX < viewLocation[0] + quickCamera.getWidth() && + screenY >= viewLocation[1] && screenY < viewLocation[1] + quickCamera.getHeight(); } private int computeCameraTopPosition(int slideOffset) { @@ -469,7 +469,7 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { return getPaddingTop(); } - final int baseCameraTop = (cameraView.getMeasuredHeight() - halfExpandedHeight) / 2; + final int baseCameraTop = (quickCamera.getMeasuredHeight() - halfExpandedHeight) / 2; final int baseOffset = getMeasuredHeight() - slideOffset - baseCameraTop; final float slop = Util.clamp((float)(slideOffset - halfExpandedHeight) / (getMeasuredHeight() - halfExpandedHeight), 0f, @@ -502,12 +502,12 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { public void onPause() { paused = true; - cameraView.onPause(); + quickCamera.onPause(); } public void onResume() { paused = false; - if (drawerState.isVisible()) cameraView.onResume(); + if (drawerState.isVisible()) quickCamera.onResume(); } public enum DrawerState { @@ -522,18 +522,18 @@ public class QuickAttachmentDrawer extends ViewGroup implements InputView { @Override public void onClick(View v) { boolean crop = drawerState != DrawerState.FULL_EXPANDED; - int imageHeight = crop ? getContainer().getKeyboardHeight() : cameraView.getMeasuredHeight(); - Rect previewRect = new Rect(0, 0, cameraView.getMeasuredWidth(), imageHeight); - cameraView.takePicture(previewRect); + int imageHeight = crop ? getContainer().getKeyboardHeight() : quickCamera.getMeasuredHeight(); + Rect previewRect = new Rect(0, 0, quickCamera.getMeasuredWidth(), imageHeight); + quickCamera.takePicture(previewRect); } } private class CameraFlipClickListener implements OnClickListener { @Override public void onClick(View v) { - cameraView.flipCamera(); - swapCameraButton.setImageResource(cameraView.isRearCamera() ? R.drawable.quick_camera_front - : R.drawable.quick_camera_rear); + quickCamera.swapCamera(); + swapCameraButton.setImageResource(quickCamera.isRearCamera() ? R.drawable.quick_camera_front + : R.drawable.quick_camera_rear); } } diff --git a/src/org/thoughtcrime/securesms/components/camera/QuickCamera.java b/src/org/thoughtcrime/securesms/components/camera/QuickCamera.java new file mode 100644 index 000000000..4f4dde647 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/camera/QuickCamera.java @@ -0,0 +1,207 @@ +package org.thoughtcrime.securesms.components.camera; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.os.AsyncTask; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.Log; + +import com.commonsware.cwac.camera.CameraHost.FailureReason; +import com.commonsware.cwac.camera.SimpleCameraHost; + +import org.thoughtcrime.securesms.util.BitmapUtil; + +import java.io.IOException; +import java.util.List; + +@SuppressWarnings("deprecation") public class QuickCamera extends CameraView { + private static final String TAG = QuickCamera.class.getSimpleName(); + + private QuickCameraListener listener; + private boolean capturing; + private boolean started; + private QuickCameraHost cameraHost; + + public QuickCamera(Context context) { + this(context, null); + } + + public QuickCamera(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public QuickCamera(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + cameraHost = new QuickCameraHost(context); + setHost(cameraHost); + } + + @Override + public void onResume() { + if (started) return; + super.onResume(); + started = true; + } + + @Override + public void onPause() { + if (!started) return; + super.onPause(); + started = false; + } + + public boolean isStarted() { + return started; + } + + public void takePicture(final Rect previewRect) { + if (capturing) { + Log.w(TAG, "takePicture() called while previous capture pending."); + return; + } + + final Parameters cameraParameters = getCameraParameters(); + if (cameraParameters == null) { + Log.w(TAG, "camera not in capture-ready state"); + return; + } + + setOneShotPreviewCallback(new Camera.PreviewCallback() { + @Override + public void onPreviewFrame(byte[] data, final Camera camera) { + final int rotation = getCameraPictureOrientation(); + final Size previewSize = cameraParameters.getPreviewSize(); + final Rect croppingRect = getCroppedRect(previewSize, previewRect, rotation); + + Log.w(TAG, "previewSize: " + previewSize.width + "x" + previewSize.height); + Log.w(TAG, "previewFormat: " + cameraParameters.getPreviewFormat()); + Log.w(TAG, "croppingRect: " + croppingRect.toString()); + Log.w(TAG, "rotation: " + rotation); + new AsyncTask() { + @Override + protected byte[] doInBackground(byte[]... params) { + byte[] data = params[0]; + try { + + return BitmapUtil.createFromNV21(data, + previewSize.width, + previewSize.height, + rotation, + croppingRect); + } catch (IOException e) { + return null; + } + } + + @Override + protected void onPostExecute(byte[] imageBytes) { + capturing = false; + if (imageBytes != null && listener != null) listener.onImageCapture(imageBytes); + } + }.execute(data); + } + }); + } + + private Rect getCroppedRect(Size cameraPreviewSize, Rect visibleRect, int rotation) { + final int previewWidth = cameraPreviewSize.width; + final int previewHeight = cameraPreviewSize.height; + + if (rotation % 180 > 0) rotateRect(visibleRect); + + float scale = (float) previewWidth / visibleRect.width(); + if (visibleRect.height() * scale > previewHeight) { + scale = (float) previewHeight / visibleRect.height(); + } + final float newWidth = visibleRect.width() * scale; + final float newHeight = visibleRect.height() * scale; + final float centerX; + final float centerY = previewHeight / 2; + if (VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) { + centerX = previewWidth - newWidth / 2; + } else { + centerX = previewWidth / 2; + } + + visibleRect.set((int) (centerX - newWidth / 2), + (int) (centerY - newHeight / 2), + (int) (centerX + newWidth / 2), + (int) (centerY + newHeight / 2)); + + if (rotation % 180 > 0) rotateRect(visibleRect); + return visibleRect; + } + + @SuppressWarnings("SuspiciousNameCombination") + private void rotateRect(Rect rect) { + rect.set(rect.top, rect.left, rect.bottom, rect.right); + } + + public void setQuickCameraListener(QuickCameraListener listener) { + this.listener = listener; + } + + public boolean isMultipleCameras() { + return Camera.getNumberOfCameras() > 1; + } + + public boolean isRearCamera() { + return cameraHost.getCameraId() == Camera.CameraInfo.CAMERA_FACING_BACK; + } + + public void swapCamera() { + cameraHost.swapCameraId(); + onPause(); + onResume(); + } + + public interface QuickCameraListener { + void onImageCapture(@NonNull final byte[] imageBytes); + void onCameraFail(FailureReason reason); + } + + private class QuickCameraHost extends SimpleCameraHost { + int cameraId = CameraInfo.CAMERA_FACING_BACK; + + public QuickCameraHost(Context context) { + super(context); + } + + @TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH) @Override + public Parameters adjustPreviewParameters(Parameters parameters) { + List focusModes = parameters.getSupportedFocusModes(); + if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } + return parameters; + } + + @Override + public int getCameraId() { + return cameraId; + } + + public void swapCameraId() { + if (isMultipleCameras()) { + if (cameraId == CameraInfo.CAMERA_FACING_BACK) cameraId = CameraInfo.CAMERA_FACING_FRONT; + else cameraId = CameraInfo.CAMERA_FACING_BACK; + } + } + + @Override + public void onCameraFail(FailureReason reason) { + super.onCameraFail(reason); + if (listener != null) listener.onCameraFail(reason); + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/components/camera/SurfacePreviewStrategy.java b/src/org/thoughtcrime/securesms/components/camera/SurfacePreviewStrategy.java new file mode 100644 index 000000000..78e8a2769 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/camera/SurfacePreviewStrategy.java @@ -0,0 +1,82 @@ +/*** + Copyright (c) 2013 CommonsWare, LLC + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package org.thoughtcrime.securesms.components.camera; + +import android.hardware.Camera; +import android.media.MediaRecorder; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; + +import java.io.IOException; + +class SurfacePreviewStrategy implements PreviewStrategy, + SurfaceHolder.Callback { + private final static String TAG = SurfacePreviewStrategy.class.getSimpleName(); + private final CameraView cameraView; + private SurfaceView preview=null; + private SurfaceHolder previewHolder=null; + private boolean ready = false; + + @SuppressWarnings("deprecation") + SurfacePreviewStrategy(CameraView cameraView) { + this.cameraView=cameraView; + preview=new SurfaceView(cameraView.getContext()); + previewHolder=preview.getHolder(); + previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + previewHolder.addCallback(this); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.w(TAG, "surfaceCreated()"); + ready = true; + synchronized (cameraView) { cameraView.notifyAll(); } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, + int width, int height) { + Log.w(TAG, "surfaceChanged()"); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.w(TAG, "surfaceDestroyed()"); + cameraView.onPause(); + } + + @Override + public void attach(Camera camera) throws IOException { + Log.w(TAG, "attach(Camera)"); + camera.setPreviewDisplay(previewHolder); + } + + @Override + public void attach(MediaRecorder recorder) { + recorder.setPreviewDisplay(previewHolder.getSurface()); + } + + @Override + public View getWidget() { + return(preview); + } + + @Override + public boolean isReady() { + return ready; + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/components/camera/TexturePreviewStrategy.java b/src/org/thoughtcrime/securesms/components/camera/TexturePreviewStrategy.java new file mode 100644 index 000000000..4b7049eeb --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/camera/TexturePreviewStrategy.java @@ -0,0 +1,93 @@ +package org.thoughtcrime.securesms.components.camera; +/*** + Copyright (c) 2013 CommonsWare, LLC + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.media.MediaRecorder; +import android.os.Build; +import android.util.Log; +import android.view.TextureView; +import android.view.View; + +import java.io.IOException; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +class TexturePreviewStrategy implements PreviewStrategy, + TextureView.SurfaceTextureListener { + private final static String TAG = TexturePreviewStrategy.class.getSimpleName(); + private final CameraView cameraView; + private TextureView widget=null; + private SurfaceTexture surface=null; + + TexturePreviewStrategy(CameraView cameraView) { + this.cameraView=cameraView; + widget=new TextureView(cameraView.getContext()); + widget.setSurfaceTextureListener(this); + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, + int width, int height) { + Log.w(TAG, "onSurfaceTextureAvailable()"); + this.surface=surface; + synchronized (cameraView) { cameraView.notifyAll(); } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, + int width, int height) { + Log.w(TAG, "onSurfaceTextureChanged()"); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + Log.w(TAG, "onSurfaceTextureDestroyed()"); + cameraView.onPause(); + + return(true); + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // no-op + } + + @Override + public void attach(Camera camera) throws IOException { + camera.setPreviewTexture(surface); + } + + @Override + public void attach(MediaRecorder recorder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // no-op + } + else { + throw new IllegalStateException( + "Cannot use TextureView with MediaRecorder"); + } + } + + @Override + public boolean isReady() { + return widget.isAvailable(); + } + + @Override + public View getWidget() { + return(widget); + } +} \ No newline at end of file