diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java index b74f5868..1ba4d199 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -48,7 +48,7 @@ public abstract class CameraListener { * Notifies that a picture previously captured with {@link CameraView#takePicture()} * or {@link CameraView#takePictureSnapshot()} is ready to be shown or saved to file. * - * If planning to show a bitmap, you can use {@link PictureResult#asBitmap(int, int, BitmapCallback)} + * If planning to show a bitmap, you can use {@link PictureResult#toBitmap(int, int, BitmapCallback)} * to decode the byte array taking care about orientation and threading. * * @param result captured picture diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java b/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java index 8439fa53..a28b68b1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java @@ -1,6 +1,9 @@ package com.otaliastudios.cameraview; import android.location.Location; + +import java.io.File; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -93,7 +96,7 @@ public class PictureResult { * @param maxHeight the max. height of final bitmap * @param callback a callback to be notified of image decoding */ - public void asBitmap(int maxWidth, int maxHeight, @NonNull BitmapCallback callback) { + public void toBitmap(int maxWidth, int maxHeight, @NonNull BitmapCallback callback) { CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, rotation, callback); } @@ -104,9 +107,19 @@ public class PictureResult { * * @param callback a callback to be notified of image decoding */ - public void asBitmap(@NonNull BitmapCallback callback) { - asBitmap(-1, -1, callback); + public void toBitmap(@NonNull BitmapCallback callback) { + toBitmap(-1, -1, callback); } - // TODO toFile + /** + * Shorthand for {@link CameraUtils#writeToFile(byte[], File, FileCallback)}. + * This writes this picture to file on a background thread and posts the result in the UI + * thread using the given callback. + * + * @param file the file to write into + * @param callback a callback + */ + public void toFile(@NonNull File file, @NonNull FileCallback callback) { + CameraUtils.writeToFile(getData(), file, callback); + } } diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java index 0f83e05f..57aaa964 100644 --- a/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java +++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java @@ -12,9 +12,14 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.exifinterface.media.ExifInterface; +import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; /** * Static utilities for dealing with camera I/O, orientations, etc. @@ -57,6 +62,59 @@ public class CameraUtils { } + /** + * Simply writes the given data to the given file. It is done synchronously. If you are + * running on the UI thread, please use {@link #writeToFile(byte[], File, FileCallback)} + * and pass a file callback. + * + * If any error is encountered, this returns null. + * + * @param data the data to be written + * @param file the file to write into + * @return the source file, or null if error + */ + @SuppressWarnings("WeakerAccess") + @Nullable + @WorkerThread + public static File writeToFile(@NonNull final byte[] data, @NonNull File file) { + if (file.exists() && !file.delete()) return null; + try (OutputStream stream = new BufferedOutputStream(new FileOutputStream(file))) { + stream.write(data); + stream.flush(); + return file; + } catch (IOException e) { + return null; + } + } + + + /** + * Writes the given data to the given file in a background thread, returning on the + * original thread (typically the UI thread) once writing is done. + * If some error is encountered, the {@link FileCallback} will return null instead of the + * original file. + * + * @param data the data to be written + * @param file the file to write into + * @param callback a callback + */ + @SuppressWarnings("WeakerAccess") + public static void writeToFile(@NonNull final byte[] data, @NonNull final File file, @NonNull final FileCallback callback) { + final Handler ui = new Handler(); + WorkerHandler.run(new Runnable() { + @Override + public void run() { + final File result = writeToFile(data, file); + ui.post(new Runnable() { + @Override + public void run() { + callback.onFileReady(result); + } + }); + } + }); + } + /** * Decodes an input byte array and outputs a Bitmap that is ready to be displayed. * The difference with {@link android.graphics.BitmapFactory#decodeByteArray(byte[], int, int)} @@ -65,9 +123,10 @@ public class CameraUtils { * @param source a JPEG byte array */ @SuppressWarnings("WeakerAccess") + @Nullable @WorkerThread - public static void decodeBitmap(@NonNull final byte[] source) { - decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE); + public static Bitmap decodeBitmap(@NonNull final byte[] source) { + return decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE); } /** diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/FileCallback.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/FileCallback.java new file mode 100644 index 00000000..1f84b9aa --- /dev/null +++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/FileCallback.java @@ -0,0 +1,25 @@ +package com.otaliastudios.cameraview; + +import android.graphics.Bitmap; + +import java.io.File; + +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; + +/** + * Receives callbacks about a file saving operation. + */ +public interface FileCallback { + + /** + * Notifies that the data was succesfully written to file. + * This is run on the UI thread. + * Returns a null object if an exception was encountered, for example + * if you don't have permissions to write to file. + * + * @param file the written file, or null + */ + @UiThread + void onFileReady(@Nullable File file); +} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java index aaac6c25..2805c027 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java @@ -39,7 +39,7 @@ public class PicturePreviewActivity extends Activity { captureLatency.setTitleAndMessage("Approx. latency", delay + " milliseconds"); captureResolution.setTitleAndMessage("Resolution", result.getSize() + " (" + ratio + ")"); exifRotation.setTitleAndMessage("EXIF rotation", result.getRotation() + ""); - result.asBitmap(1000, 1000, new BitmapCallback() { + result.toBitmap(1000, 1000, new BitmapCallback() { @Override public void onBitmapReady(Bitmap bitmap) { imageView.setImageBitmap(bitmap); diff --git a/docs/_posts/2018-12-20-getting-started.md b/docs/_posts/2018-12-20-getting-started.md index fe014401..6660b88b 100644 --- a/docs/_posts/2018-12-20-getting-started.md +++ b/docs/_posts/2018-12-20-getting-started.md @@ -73,12 +73,16 @@ camera.addCameraListener(new CameraListener() { @Override public void onPictureTaken(PictureResult result) { // Picture was taken! - // If planning to save a file, just get the jpeg array. - byte[] jpeg = result.getJpeg(); - // If planning to show a Bitmap, we will take care of // EXIF rotation and background threading for you... - result.asBitmap(maxWidth, maxHeight, callback); + result.toBitmap(maxWidth, maxHeight, callback); + + // If planning to save a file on a background thread, + // just use toFile. Ensure you have permissions. + result.toFile(file, callback); + + // Access the raw data if needed. + byte[] data = result.getData(); } }); camera.takePicture(); @@ -124,7 +128,7 @@ For runtime permissions and Manifest setup, please read the [permissions page]() If you are not using support libraries and you can't resolve the LifecycleOwner interface, make sure you override `onResume`, `onPause` and `onDestroy` in your activity (`onDestroyView` -in your fragment), and call `CameraView.open()`, `close()` and `destroy()`. +in your fragment), and call `open()`, `close()` and `destroy()`. ```java @Override diff --git a/docs/_posts/2018-12-20-v1-migration-guide.md b/docs/_posts/2018-12-20-v1-migration-guide.md index 9149a803..7d27c07a 100644 --- a/docs/_posts/2018-12-20-v1-migration-guide.md +++ b/docs/_posts/2018-12-20-v1-migration-guide.md @@ -156,7 +156,7 @@ The listener interface brings two breaking signature changes: - `onPictureTaken()` now returns a `PictureResult`. Use `result.getJpeg()` to access the jpeg stream. The result class includes rich information about the picture (or picture snapshot) that was taken, - plus handy utilities (`result.asBitmap()`...) + plus handy utilities (`result.toBitmap()`, `result.toFile()`...) - `onVideoTaken()` now returns a `VideoResult`. Use `result.getFile()` to access the video file. The result class includes rich information about the video (or video snapshot) that was taken.