re-enable direct capture

Closes #3664
// FREEBIE
This commit is contained in:
Jake McGinty 2015-07-13 17:35:34 -07:00 committed by Moxie Marlinspike
parent 47b21707be
commit 1a7ab6346f
10 changed files with 236 additions and 124 deletions

View File

@ -11,7 +11,7 @@
android:label="Access to TextSecure Secrets" android:label="Access to TextSecure Secrets"
android:protectionLevel="signature" /> android:protectionLevel="signature" />
<!--<uses-feature android:name="android.hardware.camera" android:required="false" />--> <uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/> <uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
<uses-permission android:name="android.permission.READ_PROFILE"/> <uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.WRITE_PROFILE"/> <uses-permission android:name="android.permission.WRITE_PROFILE"/>
@ -34,12 +34,12 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" /> <uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!--<uses-permission android:name="android.permission.CAMERA" />--> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!--&lt;!&ndash; For sending location tiles in the future &ndash;&gt;--> <!-- For sending location tiles in the future -->
<!--<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>--> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>--> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- So we can add a TextSecure 'Account' --> <!-- So we can add a TextSecure 'Account' -->
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
@ -47,34 +47,34 @@
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS"/> <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
<!--&lt;!&ndash; For conversation 'shortcuts' on the desktop &ndash;&gt;--> <!-- For conversation 'shortcuts' on the desktop -->
<!--<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>--> <uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
<!--&lt;!&ndash; For sending/receiving events &ndash;&gt;--> <!-- For sending/receiving events -->
<!--<uses-permission android:name="android.permission.WRITE_CALENDAR"/>--> <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<!--<uses-permission android:name="android.permission.READ_CALENDAR"/>--> <uses-permission android:name="android.permission.READ_CALENDAR"/>
<!--&lt;!&ndash; For fixing MMS &ndash;&gt;--> <!-- For fixing MMS -->
<!--<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>--> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>--> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<!--&lt;!&ndash; Set image as wallpaper &ndash;&gt;--> <!-- Set image as wallpaper -->
<!--<uses-permission android:name="android.permission.SET_WALLPAPER"/>--> <uses-permission android:name="android.permission.SET_WALLPAPER"/>
<!-- -->
<!--&lt;!&ndash; Permissions from RedPhone &ndash;&gt;--> <!-- Permissions from RedPhone -->
<!--<uses-permission android:name="android.permission.RECORD_AUDIO" />--> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />--> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!--<uses-permission android:name="android.permission.BLUETOOTH" />--> <uses-permission android:name="android.permission.BLUETOOTH" />
<!--<uses-permission android:name="android.permission.BROADCAST_STICKY" />--> <uses-permission android:name="android.permission.BROADCAST_STICKY" />
<!--<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />--> <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<!--<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />--> <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<!--<uses-permission android:name="android.permission.CALL_PHONE" />--> <uses-permission android:name="android.permission.CALL_PHONE" />
<!--<uses-permission android:name="android.permission.CALL_PRIVILEGED" />--> <uses-permission android:name="android.permission.CALL_PRIVILEGED" />
<!--<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />--> <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<!--<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />--> <uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<!--<uses-permission android:name="android.permission.READ_CALL_STATE"/>--> <uses-permission android:name="android.permission.READ_CALL_STATE"/>
<!--<uses-permission android:name="android.permission.READ_LOGS"/>--> <uses-permission android:name="android.permission.READ_LOGS"/>
<!--<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>--> <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE" <permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
android:protectionLevel="signature" /> android:protectionLevel="signature" />

View File

@ -109,7 +109,6 @@
android:src="?quick_camera_icon" android:src="?quick_camera_icon"
android:background="@drawable/touch_highlight_background" android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description" android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description"
android:visibility="gone"
android:padding="10dp" /> android:padding="10dp" />
</LinearLayout> </LinearLayout>

View File

@ -5,6 +5,6 @@
android:id="@+id/quick_camera" android:id="@+id/quick_camera"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="invisible" />
</merge> </merge>

View File

@ -59,6 +59,7 @@ import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast; import android.widget.Toast;
import com.afollestad.materialdialogs.AlertDialogWrapper; import com.afollestad.materialdialogs.AlertDialogWrapper;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
@ -96,6 +97,7 @@ import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.providers.CaptureProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
@ -815,7 +817,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
attachButton.setOnClickListener(new AttachButtonListener()); attachButton.setOnClickListener(new AttachButtonListener());
quickAttachmentToggle.setEnabled(false);
sendButton.setOnClickListener(sendButtonListener); sendButton.setOnClickListener(sendButtonListener);
sendButton.setEnabled(true); sendButton.setEnabled(true);
sendButton.addOnTransportChangedListener(new OnTransportChangedListener() { sendButton.addOnTransportChangedListener(new OnTransportChangedListener() {
@ -978,7 +979,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case AttachmentTypeSelectorAdapter.ADD_CONTACT_INFO: case AttachmentTypeSelectorAdapter.ADD_CONTACT_INFO:
AttachmentManager.selectContactInfo(this, PICK_CONTACT_INFO); break; AttachmentManager.selectContactInfo(this, PICK_CONTACT_INFO); break;
case AttachmentTypeSelectorAdapter.TAKE_PHOTO: case AttachmentTypeSelectorAdapter.TAKE_PHOTO:
attachmentManager.capturePhoto(this, TAKE_PHOTO); break; attachmentManager.capturePhoto(this, recipients, TAKE_PHOTO); break;
} }
} }
@ -1332,11 +1333,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
@Override @Override
public void onImageCapture(@NonNull final Bitmap bitmap) { public void onImageCapture(@NonNull final byte[] imageBytes) {
attachmentManager.setCaptureImage(masterSecret, bitmap); attachmentManager.setCaptureUri(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes));
addAttachmentImage(masterSecret, attachmentManager.getCaptureUri());
quickAttachmentDrawer.close(); quickAttachmentDrawer.close();
} }
@Override
public void onCameraFail(FailureReason reason) {
Toast.makeText(this, R.string.quick_camera_unavailable, Toast.LENGTH_SHORT).show();
quickAttachmentDrawer.close();
quickAttachmentToggle.disable();
}
// Listeners // Listeners
private class AttachmentTypeListener implements DialogInterface.OnClickListener { private class AttachmentTypeListener implements DialogInterface.OnClickListener {

View File

@ -19,7 +19,6 @@ import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.hardware.Camera; import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback; import android.hardware.Camera.PreviewCallback;
import android.os.Build; import android.os.Build;
@ -30,7 +29,6 @@ import android.util.Log;
import android.view.OrientationEventListener; import android.view.OrientationEventListener;
import android.view.Surface; import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import java.io.IOException; import java.io.IOException;
@ -91,7 +89,7 @@ public class CameraView extends FrameLayout {
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void onResume() { public void onResume() {
Log.w(TAG, "onResume()"); Log.w(TAG, "onResume() queued");
final CameraHost host = getHost(); final CameraHost host = getHost();
submitTask(new SerializedAsyncTask<FailureReason>() { submitTask(new SerializedAsyncTask<FailureReason>() {
@Override protected FailureReason onRunBackground() { @Override protected FailureReason onRunBackground() {
@ -110,11 +108,11 @@ public class CameraView extends FrameLayout {
} }
@Override protected void onPostMain(FailureReason result) { @Override protected void onPostMain(FailureReason result) {
cameraReady = true;
if (result != null) { if (result != null) {
host.onCameraFail(result); host.onCameraFail(result);
return; return;
} }
cameraReady = true;
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
onOrientationChange.enable(); onOrientationChange.enable();
} }
@ -126,12 +124,13 @@ public class CameraView extends FrameLayout {
initPreview(); initPreview();
requestLayout(); requestLayout();
invalidate(); invalidate();
Log.w(TAG, "onResume() completed");
} }
}); });
} }
public void onPause() { public void onPause() {
Log.w(TAG, "onPause()"); Log.w(TAG, "onPause() queued");
submitTask(new SerializedAsyncTask<Void>() { submitTask(new SerializedAsyncTask<Void>() {
@Override protected void onPreMain() { @Override protected void onPreMain() {
cameraReady = false; cameraReady = false;
@ -151,6 +150,7 @@ public class CameraView extends FrameLayout {
outputOrientation = -1; outputOrientation = -1;
cameraId = -1; cameraId = -1;
lastPictureOrientation = -1; lastPictureOrientation = -1;
Log.w(TAG, "onPause() completed");
} }
}); });
} }
@ -255,6 +255,7 @@ public class CameraView extends FrameLayout {
} }
void previewCreated() { void previewCreated() {
Log.w(TAG, "previewCreated() queued");
final CameraHost host = getHost(); final CameraHost host = getHost();
submitTask(new PostInitializationTask<Void>() { submitTask(new PostInitializationTask<Void>() {
@Override protected void onPostMain(Void avoid) { @Override protected void onPostMain(Void avoid) {
@ -265,6 +266,7 @@ public class CameraView extends FrameLayout {
} catch (IOException e) { } catch (IOException e) {
host.handleException(e); host.handleException(e);
} }
Log.w(TAG, "previewCreated() completed");
} }
}); });
} }
@ -277,11 +279,6 @@ public class CameraView extends FrameLayout {
} }
} }
void previewReset() {
previewStopped();
initPreview();
}
private void previewStopped() { private void previewStopped() {
if (inPreview) { if (inPreview) {
stopPreview(); stopPreview();
@ -290,6 +287,7 @@ public class CameraView extends FrameLayout {
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void initPreview() { public void initPreview() {
Log.w(TAG, "initPreview() queued");
submitTask(new PostInitializationTask<Void>() { submitTask(new PostInitializationTask<Void>() {
@Override protected void onPostMain(Void avoid) { @Override protected void onPostMain(Void avoid) {
if (camera != null && cameraReady) { if (camera != null && cameraReady) {
@ -305,20 +303,19 @@ public class CameraView extends FrameLayout {
startPreview(); startPreview();
requestLayout(); requestLayout();
invalidate(); invalidate();
Log.w(TAG, "initPreview() completed");
} }
} }
}); });
} }
private void startPreview() { private void startPreview() {
Log.w(TAG, "startPreview()");
camera.startPreview(); camera.startPreview();
inPreview = true; inPreview = true;
getHost().autoFocusAvailable(); getHost().autoFocusAvailable();
} }
private void stopPreview() { private void stopPreview() {
Log.w(TAG, "stopPreview()");
camera.startPreview(); camera.startPreview();
inPreview = false; inPreview = false;
getHost().autoFocusUnavailable(); getHost().autoFocusUnavailable();
@ -453,20 +450,24 @@ public class CameraView extends FrameLayout {
@Override public void onAdded() {} @Override public void onAdded() {}
@Override public final void onRun() { @Override public final void onRun() {
onWait(); try {
runOnMainSync(new Runnable() { onWait();
@Override public void run() { runOnMainSync(new Runnable() {
onPreMain(); @Override public void run() {
} onPreMain();
}); }
});
final Result result = onRunBackground(); final Result result = onRunBackground();
runOnMainSync(new Runnable() { runOnMainSync(new Runnable() {
@Override public void run() { @Override public void run() {
onPostMain(result); onPostMain(result);
} }
}); });
} catch (PreconditionsNotMetException e) {
Log.w(TAG, "skipping task, preconditions not met in onWait()");
}
} }
@Override public boolean onShouldRetry(Exception e) { @Override public boolean onShouldRetry(Exception e) {
@ -493,19 +494,26 @@ public class CameraView extends FrameLayout {
} }
} }
protected void onWait() {} protected void onWait() throws PreconditionsNotMetException {}
protected void onPreMain() {} protected void onPreMain() {}
protected Result onRunBackground() { return null; } protected Result onRunBackground() { return null; }
protected void onPostMain(Result result) {} protected void onPostMain(Result result) {}
} }
private abstract class PostInitializationTask<Result> extends SerializedAsyncTask<Result> { private abstract class PostInitializationTask<Result> extends SerializedAsyncTask<Result> {
@Override protected void onWait() { @Override protected void onWait() throws PreconditionsNotMetException {
synchronized (CameraView.this) { synchronized (CameraView.this) {
if (!cameraReady) {
throw new PreconditionsNotMetException();
}
while (camera == null || previewSize == null || !previewStrategy.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); Util.wait(CameraView.this, 0);
} }
} }
} }
} }
private static class PreconditionsNotMetException extends Exception {}
} }

View File

@ -52,6 +52,7 @@ public class QuickAttachmentDrawer extends ViewGroup {
private float halfExpandedAnchorPoint = COLLAPSED_ANCHOR_POINT; private float halfExpandedAnchorPoint = COLLAPSED_ANCHOR_POINT;
private boolean halfModeUnsupported = VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH; private boolean halfModeUnsupported = VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH;
private Rect drawChildrenRect = new Rect(); private Rect drawChildrenRect = new Rect();
private boolean paused = false;
public QuickAttachmentDrawer(Context context) { public QuickAttachmentDrawer(Context context) {
this(context, null); this(context, null);
@ -128,7 +129,7 @@ public class QuickAttachmentDrawer extends ViewGroup {
} }
shutterButton.setOnClickListener(new ShutterClickListener()); shutterButton.setOnClickListener(new ShutterClickListener());
fullScreenButton.setOnClickListener(new FullscreenClickListener()); fullScreenButton.setOnClickListener(new FullscreenClickListener());
controls.setVisibility(GONE); controls.setVisibility(INVISIBLE);
addView(controls, controlsIndex > -1 ? controlsIndex : indexOfChild(quickCamera) + 1); addView(controls, controlsIndex > -1 ? controlsIndex : indexOfChild(quickCamera) + 1);
} }
@ -274,10 +275,10 @@ public class QuickAttachmentDrawer extends ViewGroup {
} }
if (slideOffset == COLLAPSED_ANCHOR_POINT && quickCamera.isStarted()) { if (slideOffset == COLLAPSED_ANCHOR_POINT && quickCamera.isStarted()) {
controls.setVisibility(GONE);
quickCamera.setVisibility(GONE);
quickCamera.onPause(); quickCamera.onPause();
} else if (slideOffset != COLLAPSED_ANCHOR_POINT && !quickCamera.isStarted()) { controls.setVisibility(INVISIBLE);
quickCamera.setVisibility(INVISIBLE);
} else if (slideOffset != COLLAPSED_ANCHOR_POINT && !quickCamera.isStarted() & !paused) {
controls.setVisibility(VISIBLE); controls.setVisibility(VISIBLE);
quickCamera.setVisibility(VISIBLE); quickCamera.setVisibility(VISIBLE);
quickCamera.onResume(); quickCamera.onResume();
@ -507,10 +508,12 @@ public class QuickAttachmentDrawer extends ViewGroup {
} }
public void onPause() { public void onPause() {
paused = true;
quickCamera.onPause(); quickCamera.onPause();
} }
public void onResume() { public void onResume() {
paused = false;
if (drawerState.isVisible()) quickCamera.onResume(); if (drawerState.isVisible()) quickCamera.onResume();
} }

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.camera;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.Camera; import android.hardware.Camera;
import android.hardware.Camera.CameraInfo; import android.hardware.Camera.CameraInfo;
@ -13,11 +12,10 @@ import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.commonsware.cwac.camera.SimpleCameraHost; import com.commonsware.cwac.camera.SimpleCameraHost;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import java.io.IOException; import java.io.IOException;
@ -78,29 +76,33 @@ import java.util.List;
setOneShotPreviewCallback(new Camera.PreviewCallback() { setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override @Override
public void onPreviewFrame(byte[] data, final Camera camera) { public void onPreviewFrame(byte[] data, final Camera camera) {
final int rotation = getCameraPictureOrientation(); final int rotation = getCameraPictureOrientation();
final Size previewSize = cameraParameters.getPreviewSize();
final Rect croppingRect = getCroppedRect(previewSize, previewRect, rotation);
new AsyncTask<byte[], Void, Bitmap>() { Log.w(TAG, "previewSize: " + previewSize.width + "x" + previewSize.height);
Log.w(TAG, "croppingRect: " + croppingRect.toString());
Log.w(TAG, "rotation: " + rotation);
new AsyncTask<byte[], Void, byte[]>() {
@Override @Override
protected Bitmap doInBackground(byte[]... params) { protected byte[] doInBackground(byte[]... params) {
byte[] data = params[0]; byte[] data = params[0];
try { try {
Size previewSize = cameraParameters.getPreviewSize();
return BitmapUtil.createFromNV21(data, return BitmapUtil.createFromNV21(data,
previewSize.width, previewSize.width,
previewSize.height, previewSize.height,
rotation, rotation,
getCroppedRect(previewSize, previewRect, rotation)); croppingRect);
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
} }
@Override @Override
protected void onPostExecute(Bitmap bitmap) { protected void onPostExecute(byte[] imageBytes) {
capturing = false; capturing = false;
if (bitmap != null && listener != null) listener.onImageCapture(bitmap); if (imageBytes != null && listener != null) listener.onImageCapture(imageBytes);
} }
}.execute(data); }.execute(data);
} }
@ -111,10 +113,7 @@ import java.util.List;
final int previewWidth = cameraPreviewSize.width; final int previewWidth = cameraPreviewSize.width;
final int previewHeight = cameraPreviewSize.height; final int previewHeight = cameraPreviewSize.height;
if (rotation % 180 > 0) { if (rotation % 180 > 0) rotateRect(visibleRect);
//noinspection SuspiciousNameCombination
visibleRect.set(visibleRect.top, visibleRect.left, visibleRect.bottom, visibleRect.right);
}
float scale = (float) previewWidth / visibleRect.width(); float scale = (float) previewWidth / visibleRect.width();
if (visibleRect.height() * scale > previewHeight) { if (visibleRect.height() * scale > previewHeight) {
@ -128,9 +127,16 @@ import java.util.List;
(int) (centerY - newHeight / 2), (int) (centerY - newHeight / 2),
(int) (centerX + newWidth / 2), (int) (centerX + newWidth / 2),
(int) (centerY + newHeight / 2)); (int) (centerY + newHeight / 2));
if (rotation % 180 > 0) rotateRect(visibleRect);
return 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) { public void setQuickCameraListener(QuickCameraListener listener) {
this.listener = listener; this.listener = listener;
} }
@ -150,7 +156,8 @@ import java.util.List;
} }
public interface QuickCameraListener { public interface QuickCameraListener {
void onImageCapture(@NonNull final Bitmap bitmap); void onImageCapture(@NonNull final byte[] imageBytes);
void onCameraFail(FailureReason reason);
} }
private class QuickCameraHost extends SimpleCameraHost { private class QuickCameraHost extends SimpleCameraHost {
@ -186,7 +193,7 @@ import java.util.List;
@Override @Override
public void onCameraFail(FailureReason reason) { public void onCameraFail(FailureReason reason) {
super.onCameraFail(reason); super.onCameraFail(reason);
Toast.makeText(getContext(), R.string.quick_camera_unavailable, Toast.LENGTH_SHORT).show(); if (listener != null) listener.onCameraFail(reason);
} }
} }
} }

View File

@ -20,7 +20,6 @@ import android.app.Activity;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.ContactsContract; import android.provider.ContactsContract;
@ -30,9 +29,6 @@ import android.util.Log;
import android.view.View; import android.view.View;
import android.view.animation.AlphaAnimation; import android.view.animation.AlphaAnimation;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.ScaleAnimation;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Toast; import android.widget.Toast;
@ -40,9 +36,9 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.providers.CaptureProvider;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import java.io.File;
import java.io.IOException; import java.io.IOException;
public class AttachmentManager { public class AttachmentManager {
@ -94,8 +90,7 @@ public class AttachmentManager {
} }
public void cleanup() { public void cleanup() {
// if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri); if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri);
if (captureUri != null) new File(captureUri.getPath()).delete();
captureUri = null; captureUri = null;
} }
@ -127,19 +122,11 @@ public class AttachmentManager {
return attachmentView.getVisibility() == View.VISIBLE; return attachmentView.getVisibility() == View.VISIBLE;
} }
public SlideDeck getSlideDeck() { public SlideDeck getSlideDeck() {
return slideDeck; return slideDeck;
} }
public void setCaptureImage(MasterSecret masterSecret, Bitmap bitmap) {
try {
captureUri = CaptureProvider.getInstance(context).create(masterSecret, bitmap);
setImage(masterSecret, captureUri);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
}
}
public static void selectVideo(Activity activity, int requestCode) { public static void selectVideo(Activity activity, int requestCode) {
selectMediaType(activity, "video/*", requestCode); selectMediaType(activity, "video/*", requestCode);
} }
@ -161,12 +148,16 @@ public class AttachmentManager {
return captureUri; return captureUri;
} }
public void capturePhoto(Activity activity, int requestCode) {
public void setCaptureUri(Uri captureUri) {
this.captureUri = captureUri;
}
public void capturePhoto(Activity activity, Recipients recipients, int requestCode) {
try { try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
File captureFile = File.createTempFile(String.valueOf(System.currentTimeMillis()), ".jpg", activity.getExternalFilesDir(null)); captureUri = CaptureProvider.getInstance(context).createForExternal(recipients);
captureUri = Uri.fromFile(captureFile);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri); captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode); activity.startActivityForResult(captureIntent, requestCode);
} }

View File

@ -2,27 +2,39 @@ package org.thoughtcrime.securesms.providers;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.content.UriMatcher;
import android.graphics.Bitmap.CompressFormat;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v4.util.SparseArrayCompat;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays;
public class CaptureProvider { public class CaptureProvider {
private static final String TAG = CaptureProvider.class.getSimpleName(); private static final String TAG = CaptureProvider.class.getSimpleName();
private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture"; private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture";
public static final Uri CONTENT_URI = Uri.parse(URI_STRING); public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
public static final String AUTHORITY = "org.thoughtcrime.securesms"; public static final String AUTHORITY = "org.thoughtcrime.securesms";
public static final String EXPECTED_PATH = "capture/#"; public static final String EXPECTED_PATH = "capture/*/#";
private static final int MATCH = 1;
public static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH) {{
addURI(AUTHORITY, EXPECTED_PATH, MATCH);
}};
private static volatile CaptureProvider instance; private static volatile CaptureProvider instance;
public static CaptureProvider getInstance(Context context) { public static CaptureProvider getInstance(Context context) {
if (instance == null) { if (instance == null) {
synchronized (CaptureProvider.class) { synchronized (CaptureProvider.class) {
@ -35,28 +47,68 @@ public class CaptureProvider {
} }
private final Context context; private final Context context;
private final SparseArrayCompat<byte[]> cache = new SparseArrayCompat<>();
private CaptureProvider(Context context) { private CaptureProvider(Context context) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
} }
public Uri create(MasterSecret masterSecret, Bitmap bitmap) throws IOException { public Uri create(@NonNull MasterSecret masterSecret,
long id = System.currentTimeMillis(); @NonNull Recipients recipients,
OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret); @NonNull byte[] imageBytes)
bitmap.compress(CompressFormat.JPEG, 100, output); {
output.close(); final int id = generateId(recipients);
return ContentUris.withAppendedId(CONTENT_URI, id); cache.put(id, imageBytes);
persistToDisk(masterSecret, id, imageBytes);
final Uri uniqueUri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(System.currentTimeMillis()));
return ContentUris.withAppendedId(uniqueUri, id);
} }
public boolean delete(Uri uri) { private void persistToDisk(final MasterSecret masterSecret, final int id, final byte[] imageBytes) {
return getFile(ContentUris.parseId(uri)).delete(); new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
try {
final OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret);
Util.copy(new ByteArrayInputStream(imageBytes), output);
} catch (IOException e) {
Log.w(TAG, e);
}
return null;
}
@Override protected void onPostExecute(Void aVoid) {
cache.remove(id);
}
}.execute();
}
public Uri createForExternal(@NonNull Recipients recipients) throws IOException {
final File externalDir = context.getExternalFilesDir(null);
if (externalDir == null) throw new IOException("no external files directory");
return Uri.fromFile(new File(externalDir, String.valueOf(generateId(recipients)) + ".jpg"))
.buildUpon()
.appendQueryParameter("unique", String.valueOf(System.currentTimeMillis()))
.build();
}
public boolean delete(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case MATCH: return getFile(ContentUris.parseId(uri)).delete();
default: return new File(uri.getPath()).delete();
}
} }
public InputStream getStream(MasterSecret masterSecret, long id) throws IOException { public InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
return new DecryptingPartInputStream(getFile(id), masterSecret); final byte[] cached = cache.get((int)id);
return cached != null ? new ByteArrayInputStream(cached)
: new DecryptingPartInputStream(getFile(id), masterSecret);
}
private int generateId(Recipients recipients) {
return Math.abs(Arrays.hashCode(recipients.getIds()));
} }
private File getFile(long id) { private File getFile(long id) {
return new File(context.getDir("captures", Context.MODE_PRIVATE), id + ".capture"); return new File(context.getDir("captures", Context.MODE_PRIVATE), id + ".jpg");
} }
} }

View File

@ -100,7 +100,7 @@ public class BitmapUtil {
private static Bitmap createScaledBitmap(InputStream measure, InputStream orientationStream, InputStream data, private static Bitmap createScaledBitmap(InputStream measure, InputStream orientationStream, InputStream data,
int maxWidth, int maxHeight, boolean constrainedMemory) int maxWidth, int maxHeight, boolean constrainedMemory)
throws BitmapDecodingException throws IOException, BitmapDecodingException
{ {
Bitmap bitmap = createScaledBitmap(measure, data, maxWidth, maxHeight, constrainedMemory); Bitmap bitmap = createScaledBitmap(measure, data, maxWidth, maxHeight, constrainedMemory);
return fixOrientation(bitmap, orientationStream); return fixOrientation(bitmap, orientationStream);
@ -202,9 +202,9 @@ public class BitmapUtil {
return scaler; return scaler;
} }
private static Bitmap fixOrientation(Bitmap bitmap, InputStream orientationStream) { private static Bitmap fixOrientation(Bitmap bitmap, InputStream orientationStream) throws IOException {
final int orientation = Exif.getOrientation(orientationStream); final int orientation = Exif.getOrientation(orientationStream);
orientationStream.close();
if (orientation != 0) { if (orientation != 0) {
return rotateBitmap(bitmap, orientation); return rotateBitmap(bitmap, orientation);
} else { } else {
@ -272,22 +272,65 @@ public class BitmapUtil {
return output; return output;
} }
public static Bitmap createFromNV21(@NonNull final byte[] data, public static byte[] createFromNV21(@NonNull final byte[] data,
final int width, final int width,
final int height, final int height,
final int rotation, int rotation,
final Rect croppingRect) final Rect croppingRect)
throws IOException throws IOException
{ {
YuvImage previewImage = new YuvImage(data, ImageFormat.NV21, byte[] rotated = rotateNV21(data, width, height, rotation);
width, height, null); final int rotatedWidth = rotation % 180 > 0 ? height : width;
final int rotatedHeight = rotation % 180 > 0 ? width : height;
YuvImage previewImage = new YuvImage(rotated, ImageFormat.NV21,
rotatedWidth, rotatedHeight, null);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
previewImage.compressToJpeg(croppingRect, 100, outputStream); previewImage.compressToJpeg(croppingRect, 80, outputStream);
byte[] bytes = outputStream.toByteArray(); byte[] bytes = outputStream.toByteArray();
outputStream.close(); outputStream.close();
outputStream = new ByteArrayOutputStream(); return bytes;
return BitmapUtil.rotateBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length), rotation); }
public static byte[] rotateNV21(@NonNull final byte[] yuv,
final int width,
final int height,
final int rotation)
{
if (rotation == 0) return yuv;
if (rotation % 90 != 0 || rotation < 0 || rotation > 270) {
throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0");
}
final byte[] output = new byte[yuv.length];
final int frameSize = width * height;
final boolean swap = rotation % 180 != 0;
final boolean xflip = rotation % 270 != 0;
final boolean yflip = rotation >= 180;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
final int yIn = j * width + i;
final int uIn = frameSize + (j >> 1) * width + (i & ~1);
final int vIn = uIn + 1;
final int wOut = swap ? height : width;
final int hOut = swap ? width : height;
final int iSwapped = swap ? j : i;
final int jSwapped = swap ? i : j;
final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped;
final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped;
final int yOut = jOut * wOut + iOut;
final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1);
final int vOut = uOut + 1;
output[yOut] = (byte)(0xff & yuv[yIn]);
output[uOut] = (byte)(0xff & yuv[uIn]);
output[vOut] = (byte)(0xff & yuv[vIn]);
}
}
return output;
} }
public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) { public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) {