go back to old CameraView

Fixes #4415
Closes #4484
// FREEBIE
This commit is contained in:
Jake McGinty 2015-11-10 15:22:45 -08:00 committed by Moxie Marlinspike
parent 534421eb57
commit b0137c08cb
10 changed files with 695 additions and 366 deletions

View File

@ -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',

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<org.thoughtcrime.securesms.components.camera.CameraView
<org.thoughtcrime.securesms.components.camera.QuickCamera
android:id="@+id/quick_camera"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -54,6 +54,7 @@ import android.widget.TextView;
import android.widget.Toast;
import com.afollestad.materialdialogs.AlertDialogWrapper;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.google.protobuf.ByteString;
import org.thoughtcrime.redphone.RedPhone;
@ -1340,7 +1341,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
@Override
public void onCameraFail() {
public void onCameraFail(FailureReason reason) {
Toast.makeText(this, R.string.ConversationActivity_quick_camera_unavailable, Toast.LENGTH_SHORT).show();
quickAttachmentDrawer.hide(false);
quickAttachmentToggle.disable();

View File

@ -1,56 +0,0 @@
package org.thoughtcrime.securesms.components.camera;
import android.hardware.Camera;
import android.hardware.Camera.Size;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@SuppressWarnings("deprecation")
public class CameraUtils {
/*
* modified from: https://github.com/commonsguy/cwac-camera/blob/master/camera/src/com/commonsware/cwac/camera/CameraUtils.java
*/
public static @Nullable Size getPreferredPreviewSize(int displayOrientation,
int width,
int height,
@NonNull Camera camera) {
double targetRatio = (double)width / height;
Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
if (displayOrientation == 90 || displayOrientation == 270) {
targetRatio = (double)height / width;
}
List<Size> 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<Size> {
@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;
}
}
}

View File

@ -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> 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<Camera>() {
@Override
protected @Nullable Camera onRunBackground() {
final CameraHost host = getHost();
submitTask(new SerializedAsyncTask<FailureReason>() {
@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<Void>() {
private Optional<Camera> cameraToDestroy;
submitTask(new SerializedAsyncTask<Void>() {
@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<String> 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>() {
void previewCreated() {
Log.w(TAG, "previewCreated() queued");
final CameraHost host = getHost();
submitTask(new PostInitializationTask<Void>() {
@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<Void>() {
@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<Result> extends Job {
private static abstract class SerializedAsyncTask<Result> 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<Result> extends SerialAsyncTask<Result> {
private abstract class PostInitializationTask<Result> extends SerializedAsyncTask<Result> {
@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<byte[], Void, byte[]> {
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();
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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<byte[], Void, byte[]>() {
@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<String> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}