diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 19af8e46c5..f6bf731535 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -11,7 +11,7 @@ android:label="Access to TextSecure Secrets" android:protectionLevel="signature" /> - + @@ -34,12 +34,12 @@ - + - - - + + + @@ -47,34 +47,34 @@ - - + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index 3e540c2109..6912cfa44d 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -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" /> diff --git a/res/layout/quick_attachment_drawer.xml b/res/layout/quick_attachment_drawer.xml index 717fb1720d..1bb3907592 100644 --- a/res/layout/quick_attachment_drawer.xml +++ b/res/layout/quick_attachment_drawer.xml @@ -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" /> \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index dd164ede53..ecbc5f54dd 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -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 { diff --git a/src/org/thoughtcrime/securesms/components/camera/CameraView.java b/src/org/thoughtcrime/securesms/components/camera/CameraView.java index a60f4b75f7..7d2a5e78b6 100644 --- a/src/org/thoughtcrime/securesms/components/camera/CameraView.java +++ b/src/org/thoughtcrime/securesms/components/camera/CameraView.java @@ -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() { @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() { @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() { @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() { @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,20 +450,24 @@ public class CameraView extends FrameLayout { @Override public void onAdded() {} @Override public final void onRun() { - onWait(); - runOnMainSync(new Runnable() { - @Override public void run() { - onPreMain(); - } - }); + try { + onWait(); + runOnMainSync(new Runnable() { + @Override public void run() { + onPreMain(); + } + }); - final Result result = onRunBackground(); + final Result result = onRunBackground(); - runOnMainSync(new Runnable() { - @Override public void run() { - onPostMain(result); - } - }); + runOnMainSync(new Runnable() { + @Override public void run() { + 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 extends SerializedAsyncTask { - @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 {} } diff --git a/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java b/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java index eb44396473..60c24b3f51 100644 --- a/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java +++ b/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java @@ -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(); } diff --git a/src/org/thoughtcrime/securesms/components/camera/QuickCamera.java b/src/org/thoughtcrime/securesms/components/camera/QuickCamera.java index 4eba2df59a..1f3e108859 100644 --- a/src/org/thoughtcrime/securesms/components/camera/QuickCamera.java +++ b/src/org/thoughtcrime/securesms/components/camera/QuickCamera.java @@ -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; @@ -78,29 +76,33 @@ import java.util.List; setOneShotPreviewCallback(new Camera.PreviewCallback() { @Override 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() { + Log.w(TAG, "previewSize: " + previewSize.width + "x" + previewSize.height); + Log.w(TAG, "croppingRect: " + croppingRect.toString()); + Log.w(TAG, "rotation: " + rotation); + new AsyncTask() { @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); } } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 4a8aa5291f..cfe8477515 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -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); } diff --git a/src/org/thoughtcrime/securesms/providers/CaptureProvider.java b/src/org/thoughtcrime/securesms/providers/CaptureProvider.java index 566e08ef27..685914ae31 100644 --- a/src/org/thoughtcrime/securesms/providers/CaptureProvider.java +++ b/src/org/thoughtcrime/securesms/providers/CaptureProvider.java @@ -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 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() { + @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"); } } diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index 5582f221af..fef86d5da8 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -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) {