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: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="android.permission.READ_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.READ_CALL_LOG" />
<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" />
<!--&lt;!&ndash; For sending location tiles in the future &ndash;&gt;-->
<!--<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>-->
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>-->
<!-- For sending location tiles in the future -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- So we can add a TextSecure 'Account' -->
<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.USE_CREDENTIALS"/>
<!--&lt;!&ndash; For conversation 'shortcuts' on the desktop &ndash;&gt;-->
<!--<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>-->
<!-- For conversation 'shortcuts' on the desktop -->
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
<!--&lt;!&ndash; For sending/receiving events &ndash;&gt;-->
<!--<uses-permission android:name="android.permission.WRITE_CALENDAR"/>-->
<!--<uses-permission android:name="android.permission.READ_CALENDAR"/>-->
<!-- For sending/receiving events -->
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<!--&lt;!&ndash; For fixing MMS &ndash;&gt;-->
<!--<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>-->
<!--<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>-->
<!-- For fixing MMS -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<!--&lt;!&ndash; Set image as wallpaper &ndash;&gt;-->
<!--<uses-permission android:name="android.permission.SET_WALLPAPER"/>-->
<!-- -->
<!--&lt;!&ndash; Permissions from RedPhone &ndash;&gt;-->
<!--<uses-permission android:name="android.permission.RECORD_AUDIO" />-->
<!--<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />-->
<!--<uses-permission android:name="android.permission.BLUETOOTH" />-->
<!--<uses-permission android:name="android.permission.BROADCAST_STICKY" />-->
<!--<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />-->
<!--<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />-->
<!--<uses-permission android:name="android.permission.CALL_PHONE" />-->
<!--<uses-permission android:name="android.permission.CALL_PRIVILEGED" />-->
<!--<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />-->
<!--<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_LOGS"/>-->
<!--<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>-->
<!-- Set image as wallpaper -->
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<!-- Permissions from RedPhone -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<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_LOGS"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
android:protectionLevel="signature" />

View File

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

View File

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

View File

@ -59,6 +59,7 @@ import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import com.afollestad.materialdialogs.AlertDialogWrapper;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.google.protobuf.ByteString;
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.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.providers.CaptureProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
@ -815,7 +817,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
attachButton.setOnClickListener(new AttachButtonListener());
quickAttachmentToggle.setEnabled(false);
sendButton.setOnClickListener(sendButtonListener);
sendButton.setEnabled(true);
sendButton.addOnTransportChangedListener(new OnTransportChangedListener() {
@ -978,7 +979,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case AttachmentTypeSelectorAdapter.ADD_CONTACT_INFO:
AttachmentManager.selectContactInfo(this, PICK_CONTACT_INFO); break;
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
public void onImageCapture(@NonNull final Bitmap bitmap) {
attachmentManager.setCaptureImage(masterSecret, bitmap);
public void onImageCapture(@NonNull final byte[] imageBytes) {
attachmentManager.setCaptureUri(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes));
addAttachmentImage(masterSecret, attachmentManager.getCaptureUri());
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
private class AttachmentTypeListener implements DialogInterface.OnClickListener {

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.camera;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
@ -13,11 +12,10 @@ import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.Toast;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.commonsware.cwac.camera.SimpleCameraHost;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapUtil;
import java.io.IOException;
@ -79,28 +77,32 @@ import java.util.List;
@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);
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
protected Bitmap doInBackground(byte[]... params) {
protected byte[] doInBackground(byte[]... params) {
byte[] data = params[0];
try {
Size previewSize = cameraParameters.getPreviewSize();
return BitmapUtil.createFromNV21(data,
previewSize.width,
previewSize.height,
rotation,
getCroppedRect(previewSize, previewRect, rotation));
croppingRect);
} catch (IOException e) {
return null;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
protected void onPostExecute(byte[] imageBytes) {
capturing = false;
if (bitmap != null && listener != null) listener.onImageCapture(bitmap);
if (imageBytes != null && listener != null) listener.onImageCapture(imageBytes);
}
}.execute(data);
}
@ -111,10 +113,7 @@ import java.util.List;
final int previewWidth = cameraPreviewSize.width;
final int previewHeight = cameraPreviewSize.height;
if (rotation % 180 > 0) {
//noinspection SuspiciousNameCombination
visibleRect.set(visibleRect.top, visibleRect.left, visibleRect.bottom, visibleRect.right);
}
if (rotation % 180 > 0) rotateRect(visibleRect);
float scale = (float) previewWidth / visibleRect.width();
if (visibleRect.height() * scale > previewHeight) {
@ -128,9 +127,16 @@ import java.util.List;
(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;
}
@ -150,7 +156,8 @@ import java.util.List;
}
public interface QuickCameraListener {
void onImageCapture(@NonNull final Bitmap bitmap);
void onImageCapture(@NonNull final byte[] imageBytes);
void onCameraFail(FailureReason reason);
}
private class QuickCameraHost extends SimpleCameraHost {
@ -186,7 +193,7 @@ import java.util.List;
@Override
public void onCameraFail(FailureReason 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.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.provider.ContactsContract;
@ -30,9 +29,6 @@ import android.util.Log;
import android.view.View;
import android.view.animation.AlphaAnimation;
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.Toast;
@ -40,9 +36,9 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.CaptureProvider;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import java.io.File;
import java.io.IOException;
public class AttachmentManager {
@ -94,8 +90,7 @@ public class AttachmentManager {
}
public void cleanup() {
// if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri);
if (captureUri != null) new File(captureUri.getPath()).delete();
if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri);
captureUri = null;
}
@ -127,19 +122,11 @@ public class AttachmentManager {
return attachmentView.getVisibility() == View.VISIBLE;
}
public SlideDeck getSlideDeck() {
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) {
selectMediaType(activity, "video/*", requestCode);
}
@ -161,12 +148,16 @@ public class AttachmentManager {
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 {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
File captureFile = File.createTempFile(String.valueOf(System.currentTimeMillis()), ".jpg", activity.getExternalFilesDir(null));
captureUri = Uri.fromFile(captureFile);
captureUri = CaptureProvider.getInstance(context).createForExternal(recipients);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode);
}

View File

@ -2,27 +2,39 @@ package org.thoughtcrime.securesms.providers;
import android.content.ContentUris;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.content.UriMatcher;
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.EncryptingPartOutputStream;
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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
public class CaptureProvider {
private static final String TAG = CaptureProvider.class.getSimpleName();
private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture";
public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
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;
public static CaptureProvider getInstance(Context context) {
if (instance == null) {
synchronized (CaptureProvider.class) {
@ -35,28 +47,68 @@ public class CaptureProvider {
}
private final Context context;
private final SparseArrayCompat<byte[]> cache = new SparseArrayCompat<>();
private CaptureProvider(Context context) {
this.context = context.getApplicationContext();
}
public Uri create(MasterSecret masterSecret, Bitmap bitmap) throws IOException {
long id = System.currentTimeMillis();
OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret);
bitmap.compress(CompressFormat.JPEG, 100, output);
output.close();
return ContentUris.withAppendedId(CONTENT_URI, id);
public Uri create(@NonNull MasterSecret masterSecret,
@NonNull Recipients recipients,
@NonNull byte[] imageBytes)
{
final int id = generateId(recipients);
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) {
return getFile(ContentUris.parseId(uri)).delete();
private void persistToDisk(final MasterSecret masterSecret, final int id, final byte[] imageBytes) {
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 {
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) {
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,
int maxWidth, int maxHeight, boolean constrainedMemory)
throws BitmapDecodingException
throws IOException, BitmapDecodingException
{
Bitmap bitmap = createScaledBitmap(measure, data, maxWidth, maxHeight, constrainedMemory);
return fixOrientation(bitmap, orientationStream);
@ -202,9 +202,9 @@ public class BitmapUtil {
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);
orientationStream.close();
if (orientation != 0) {
return rotateBitmap(bitmap, orientation);
} else {
@ -272,22 +272,65 @@ public class BitmapUtil {
return output;
}
public static Bitmap createFromNV21(@NonNull final byte[] data,
public static byte[] createFromNV21(@NonNull final byte[] data,
final int width,
final int height,
final int rotation,
int rotation,
final Rect croppingRect)
throws IOException
{
YuvImage previewImage = new YuvImage(data, ImageFormat.NV21,
width, height, null);
byte[] rotated = rotateNV21(data, width, height, rotation);
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();
previewImage.compressToJpeg(croppingRect, 100, outputStream);
previewImage.compressToJpeg(croppingRect, 80, outputStream);
byte[] bytes = outputStream.toByteArray();
outputStream.close();
outputStream = new ByteArrayOutputStream();
return BitmapUtil.rotateBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length), rotation);
return bytes;
}
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) {