diff --git a/README.md b/README.md index e5772d3..06a21b0 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,27 @@ [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-RxPaparazzo-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3523) -RxJava extension for Android to access camera and gallery to take images. +RxJava extension for Android to take photos using the camera, select files or photos from the device and optionally crop or rotate any selected images. # RxPaparazzo +What is RX? + +> Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM. + What is a Paparazzo? > A freelance photographer who aggressively pursues celebrities for the purpose of taking candid photographs. -This library does that. Not really. But it was a funny name, thought. Was it? +This library does that (well not really). But it is a cool name. ## Features: - Runtime permissions. Not worries about the tricky Android runtime permissions system. RxPaparazzo relies on [RxPermissions](https://github.com/tbruyelle/RxPermissions) to deal with that. -- Take a photo using the built-in camera. -- Access to gallery. -- Crop images. RxPaparazzo relies on [UCrop](https://github.com/Yalantis/uCrop) to perform beautiful cuts to any face, body or place. +- Takes a photo using the built-in camera. +- Access to gallery and other sources of photos. +- Access to files and documents stored locally and on the cloud. +- Crop and rotate images. RxPaparazzo relies on [UCrop](https://github.com/Yalantis/uCrop) to perform beautiful cuts to any face, body or place. - Honors the observable chain (it means you can go crazy chaining operators). [RxOnActivityResult](https://github.com/VictorAlbertos/RxActivityResult) allows RxPaparazzo to transform every intent into an observable for a wonderful chaining process. @@ -31,7 +36,7 @@ allprojects { } ``` -And add next dependencies in the build.gradle of the module: +Add dependencies in the build.gradle of the module: ```gradle dependencies { compile "com.github.miguelbcr:RxPaparazzo:0.4.4" @@ -50,7 +55,7 @@ allprojects { } ``` -And add next dependencies in the build.gradle of the module: +Add dependencies in the build.gradle of the module: ```gradle dependencies { compile "com.github.miguelbcr:RxPaparazzo:0.4.4-2.x" @@ -62,7 +67,7 @@ dependencies { ## Usage Because RxPaparazzo uses RxActivityResult to deal with intent calls, all its requirements and features are inherited too. -Before attempting to use RxPaparazzo, you need to call `RxPaparazzo.register` in your Android `Application` class, supplying as parameter the current instance. +Before attempting to use RxPaparazzo, you need to call `RxPaparazzo.register` in your Android Application's `onCreate` supplying the current Application instance. ```java public class SampleApp extends Application { @@ -74,19 +79,53 @@ public class SampleApp extends Application { } ``` -Every feature RxPaparazzo exposes can be accessed from both, an `activity` or a `fragment` instance. +You will need to also add a FileProvider named `android.support.v4.content.FileProvider` to your `AndroidManifest.xml` and create a paths xml file in your src/main/res/xml directory. + +```xml + + + +``` + +If you set the provider `android:authorities` attribute to a value other than `${applicationId}.file_provider` name you must set the configuration it using `RxPaparazzo.Builder.setFileProviderAuthority(String authority)` + +Example: file_provider_paths.xml +```xml + + + + +``` + +The `file_provider_paths.xml` is where files are exposed in the FileProvider. +If you set the files-path `path` attribute to a value other than `RxPaparazzo/` you must set the configuration using `RxPaparazzo.Builder.setFileProviderDirectory(String authority)` -**Limitation:**: Your fragments need to extend from `android.support.v4.app.Fragment` instead of `android.app.Fragment`, otherwise they won't be notified. +All features RxPaparazzo exposes can be accessed from both, an `activity` or a `fragment` instance. -The generic type of the `observable` returned by RxPaparazzo when subscribing to any of its features is always an instance of [Response](https://github.com/miguelbcr/RxPaparazzo/blob/master/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/Response.java) class. +**Limitation:**: Your fragments need to extend from `android.support.v4.app.Fragment` instead of `android.app.Fragment`, otherwise they won't be notified. + +The generic type of the `observable` returned by RxPaparazzo when subscribing to any of its features is always an instance of [Response](https://github.com/miguelbcr/RxPaparazzo/blob/master/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/Response.java) class. This instance holds a reference to the current Activity/Fragment, accessible calling `targetUI()` method. Because the original one may be recreated it would be unsafe calling it. Instead, you must call any method/variable of your Activity/Fragment from this instance encapsulated in the `response` instance. Also, this instance holds a reference to the data as the appropriate response, as such as the result code of the specific operation. + +### Saving files + +By default, the image / file is saved in a directory the same as the app name on the root of the external storage. You can choose to save the images in internal storage by using `.useInternalStorage()` + +The `response` in the callback function supplied to the `subscribe()` method holds a reference to the path where the image was persisted. + ### Calling built-in camera to take a photo. ```java -RxPaparazzo.takeImage(activityOrFragment) +RxPaparazzo.single(activityOrFragment) .usingCamera() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -101,18 +140,48 @@ RxPaparazzo.takeImage(activityOrFragment) }); ``` -The `response` instance holds a reference to the path where the image was persisted. +### Calling the file picker to retrieve a file. +```java +RxPaparazzo.single(activityOrFragment) + .usingFile() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + // See response.resultCode() doc + if (response.resultCode() != RESULT_OK) { + response.targetUI().showUserCanceled(); + return; + } + + response.targetUI().loadImage(response.data()); + }); +``` -By default, the path is under app name folder on the root of the external storage, but you can save the images in internal storage by using `.useInternalStorage()` +### Calling the file picker to retrieve multiple files +```java +RxPaparazzo.multiple(activityOrFragment) + .usingFiles() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + // See response.resultCode() doc + if (response.resultCode() != RESULT_OK) { + response.targetUI().showUserCanceled(); + return; + } + if (response.data().size() == 1) response.targetUI().loadImage(response.data().get(0)); + else response.targetUI().loadImages(response.data()); + }); +``` ### Calling the gallery to retrieve an image. ```java -RxPaparazzo.takeImage(activityOrFragment) +RxPaparazzo.single(activityOrFragment) .usingGallery() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(response -> { + .subscribe(response -> { // See response.resultCode() doc if (response.resultCode() != RESULT_OK) { response.targetUI().showUserCanceled(); @@ -123,11 +192,9 @@ RxPaparazzo.takeImage(activityOrFragment) }); ``` -The `response` instance holds a reference to the path where the image was persisted. Same as the previous example. - -### Calling the gallery to retrieve multiple image +### Calling the gallery to retrieve multiple image ```java -RxPaparazzo.takeImages(activityOrFragment) +RxPaparazzo.multiple(activityOrFragment) .usingGallery() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -143,12 +210,10 @@ RxPaparazzo.takeImages(activityOrFragment) }); ``` -The `response` instance holds a reference to the paths where the images were persisted. - **Note**: if the level Android api device is minor than 18, only one image will be retrieved. ## Customizations -When asking RxPaparazzo for an image -whether it was retrieved using the built-in camera or via gallery, it's possible to apply some configurations to the action. +When asking RxPaparazzo for an photo / image / file it's possible to apply some configurations to the action. ### Size options [Size](https://github.com/miguelbcr/RxPaparazzo/blob/master/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/size/Size.java) values can be used to set the size of the image to retrieve. There are 4 options: @@ -161,7 +226,7 @@ When asking RxPaparazzo for an image -whether it was retrieved using the built-i [ScreenSize](https://github.com/miguelbcr/RxPaparazzo/blob/master/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/size/ScreenSize.java) value will be set as default. ```java -RxPaparazzo.takeImages(activityOrFragment) +RxPaparazzo.multiple(activityOrFragment) .size(new ScreenSize()) .usingGallery() ``` @@ -170,11 +235,11 @@ RxPaparazzo.takeImages(activityOrFragment) This feature is available thanks to the amazing library [uCrop](https://github.com/Yalantis/uCrop) authored by [Yalantis](https://github.com/Yalantis) group. ```java -RxPaparazzo.takeImages(activityOrFragment) +RxPaparazzo.multiple(activityOrFragment) .crop() ``` -By calling `crop()` method when building the observable instance, all they images retrieved will be able to be cropped, regardless if the images were retrieved using the built-in camera or gallery, even if multiple images were requested in a single call using `takeImages()` approach. +By calling `crop()` method when building the observable instance, all they images retrieved will be able to be cropped, regardless if the images were retrieved using the built-in camera or gallery, even if multiple images were requested in a single call using `single()` approach. Because uCrop Yalantis library exposes some configuration in order to customize the crop screen, RxPaparazzo exposes an overloaded method of `crop(UCrop.Options)` which allow to pass an instance of [UCrop.Options](https://github.com/Yalantis/uCrop/blob/master/ucrop/src/main/java/com/yalantis/ucrop/UCrop.java#L211). If you need to configure the aspect ratio, the max result size or using the source image aspect ratio, you must pass an instance of [Options](https://github.com/miguelbcr/RxPaparazzo/blob/master/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/Options.java) class, which extends from `UCrop.Options` and adds the three missing properties. @@ -182,7 +247,7 @@ If you need to configure the aspect ratio, the max result size or using the sour UCrop.Options options = new UCrop.Options(); options.setToolbarColor(ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark)); -RxPaparazzo.takeImage(activityOrFragment).crop(options) +RxPaparazzo.single(activityOrFragment).crop(options) ``` ```java @@ -190,9 +255,19 @@ Options options = new Options(); options.setToolbarColor(ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark)); options.setAspectRatio(25, 50); -RxPaparazzo.takeImage(activityOrFragment) +RxPaparazzo.single(activityOrFragment) .crop(options) ``` + +### Media scanning + +To send files to the media scanner so that they can be indexed and available in applications such as the Gallery use `sendToMediaScanner()`. If you are using `useInternalStorage()` then the media scanner will not be able to access the file. + +### Picking files + +If you wish to limit the type of images or files then use `setMimeType(String mimeType)` to specify a specific mime type for the Intent. +By default `Intent.ACTION_GET_CONTENT` is used to request images and files. If you wish to edit the original file call `useDocumentPicker()`, this will allow greater, possiblty persistent access to the source file. + ## Proguard ``` @@ -208,6 +283,13 @@ RxPaparazzo.takeImage(activityOrFragment) long consumerNode; } ``` +## Testing + +Testing has been done using the following Genymotion devices: + +* Genymotion - Google Nexus 5 5.0.0 API 21 1080x1920 480dpi +* Genymotion - Google Nexus 7 5.1.0 API 22 800x1280 213dpi + ## Credits * Runtime permissions: [RxPermissions](https://github.com/tbruyelle/RxPermissions) @@ -226,8 +308,13 @@ RxPaparazzo.takeImage(activityOrFragment) * * +**James McIntosh** + +* +* + ## Another author's libraries using RxJava: * [RxCache](https://github.com/VictorAlbertos/RxCache): Reactive caching library for Android and Java. * [RxGcm](https://github.com/VictorAlbertos/RxGcm): RxJava extension for Gcm which acts as an architectural approach to easily satisfy the requirements of an android app when dealing with push notifications. -* [RxActivityResult](https://github.com/VictorAlbertos/RxActivityResult): A reactive-tiny-badass-vindictive library to break with the OnActivityResult implementation as it breaks the observables chain. +* [RxActivityResult](https://github.com/VictorAlbertos/RxActivityResult): A tiny reactive library to break with the OnActivityResult implementation as it breaks the observables chain. diff --git a/app/build.gradle b/app/build.gradle index 3109eb5..bf83036 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,6 +48,8 @@ dependencies { compile 'com.squareup.picasso:picasso:2.5.2' compile project(':rx_paparazzo') + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5' androidTestCompile ("com.android.support.test:runner:0.4.1") { exclude module: 'support-annotations' diff --git a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/ApplicationTest.java b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/ApplicationTest.java index 59d7a69..b795524 100644 --- a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/ApplicationTest.java +++ b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/ApplicationTest.java @@ -1,35 +1,12 @@ package com.miguelbcr.ui.rx_paparazzo2.sample; -import android.app.Activity; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.graphics.drawable.BitmapDrawable; -import android.net.Uri; -import android.os.RemoteException; import android.support.test.InstrumentationRegistry; -import android.support.test.espresso.contrib.RecyclerViewActions; -import android.support.test.espresso.matcher.BoundedMatcher; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.support.test.uiautomator.UiDevice; -import android.view.Display; -import android.view.View; -import android.view.WindowManager; -import android.widget.ImageView; -import com.miguelbcr.ui.rx_paparazzo2.entities.Config; -import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; -import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; -import com.miguelbcr.ui.rx_paparazzo2.interactors.DownloadImage; -import com.miguelbcr.ui.rx_paparazzo2.interactors.GetDimens; -import com.miguelbcr.ui.rx_paparazzo2.interactors.GetPath; -import com.miguelbcr.ui.rx_paparazzo2.interactors.ImageUtils; + import com.miguelbcr.ui.rx_paparazzo2.sample.activities.StartActivity; -import com.miguelbcr.ui.rx_paparazzo2.sample.activities.Testable; -import java.io.File; -import java.util.List; -import org.hamcrest.Description; -import org.hamcrest.Matcher; + import org.junit.Before; import org.junit.FixMethodOrder; import org.junit.Rule; @@ -39,32 +16,28 @@ import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static com.miguelbcr.ui.rx_paparazzo2.sample.recyclerview.RecyclerViewUtils.withRecyclerView; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.lessThan; -import static org.hamcrest.core.CombinableMatcher.both; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; /** * TESTED ON: * - Google Nexus 5 5.0.0 API 21 1080x1920 480dpi * - Google Nexus 7 5.1.0 API 22 800x1280 213dpi + * - Samsung Galaxy S6 - 6.0.0 API 23 1440x2560 640dpi */ @RunWith(AndroidJUnit4.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ApplicationTest { - @Rule public ActivityTestRule activityRule = new ActivityTestRule<>(StartActivity.class); - private UiDevice uiDevice; - private int[] imageDimens = {0, 0}; +public class ApplicationTest extends UiActions { + + @Rule + public ActivityTestRule activityRule = new ActivityTestRule<>(StartActivity.class); @Before public void init() { - uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + // You may need to change the profile if tests are failing because they can't close the gallery picker + DeviceConfig.CURRENT = DeviceConfig.GOOGLE_NEXUS; + DeviceConfig.WAIT_TIME_FUDGE_FACTOR = 1.0; + + setUiDevice(UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())); } @Test @@ -127,206 +100,4 @@ public void _10_PickUpPhotosFragmentCrop() { pickUpPhoto(false); } - private void takePhoto(boolean crop) { - if (crop) onView(withId(R.id.fab_camera_crop)).perform(click()); - else onView(withId(R.id.fab_camera)).perform(click()); - waitTime(); - - clickBottomMiddleScreen(); - rotateDevice(); - clickBottomMiddleScreen(); - - if (crop) { - rotateDevice(); - clickTopRightScreen(); - } - - onView(withId(R.id.iv_image)).check(matches(getImageViewMatcher())); - - checkDimensions(); - } - - private void pickUpPhoto(boolean onlyOne) { - // With 4 items the recycler view do not scroll properly and do not find the item view and test crash in pos=2 - int imagesToPick = onlyOne ? 1 : 2; - - if (onlyOne) onView(withId(R.id.fab_pickup_image)).perform(click()); - else onView(withId(R.id.fab_pickup_images)).perform(click()); - - waitTime(); - - clickImagesOnScreen(imagesToPick); - - // Open selected images - if (!onlyOne) clickTopRightScreen(); - - waitTime(); - - // Close crop screen/s - for (int i = 0; i < imagesToPick; i++) { - clickTopRightScreen(); - } - - waitTime(); - - if (onlyOne) onView(withId(R.id.iv_image)).check(matches(getImageViewMatcher())); - else { - for (int i = 0; i < imagesToPick; i++) { - onView(withId(R.id.rv_images)).perform(RecyclerViewActions.scrollToPosition(i)); - - onView(withRecyclerView(R.id.rv_images) - .atPositionOnView(i, R.id.iv_image)) - .check(matches(getImageViewMatcher())); - - checkDimensions(); - } - } - } - - private Matcher getImageViewMatcher() { - return new BoundedMatcher(ImageView.class) { - @Override - public void describeTo(Description description) { - description.appendText("has drawable"); - } - - @Override - public boolean matchesSafely(ImageView imageView) { - Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); - imageDimens = new int[]{bitmap.getWidth(), bitmap.getHeight()}; - return imageView.getDrawable() != null; - } - }; - } - - private void checkDimensions() { - Activity activity = ((SampleApplication) InstrumentationRegistry.getTargetContext().getApplicationContext()).getLiveActivity(); - - if (activity instanceof Testable) { - List filePaths = ((Testable) activity).getFilePaths(); - Size size = ((Testable) activity).getSize(); - - for (String filePath : filePaths) { - assertNotNull(filePath); - assertNotEquals(filePath, ""); - - getDimens(size).with(Uri.fromFile(new File(filePath))).react() - .subscribe(dimens -> { - int[] dimensPortrait = getDimensionsPortrait(dimens[0], dimens[1]); - int[] imageDimensPortrait = getDimensionsPortrait(imageDimens[0], imageDimens[1]); - int marginOfError = 150; - assertThat(dimensPortrait[0], is(both(greaterThan(imageDimensPortrait[0] - marginOfError)).and(lessThan(imageDimensPortrait[0] + marginOfError)))); - assertThat(dimensPortrait[1], is(both(greaterThan(imageDimensPortrait[1] - marginOfError)).and(lessThan(imageDimensPortrait[1] + marginOfError)))); - }); - } - } - } - - private int[] getDimensionsPortrait(int width, int height) { - if (width < height) return new int[]{width, height}; - else return new int[]{height, width}; - } - - private GetDimens getDimens(Size size) { - Config config = new Config(); - config.setSize(size); - TargetUi targetUi = new TargetUi(activityRule.getActivity()); - ImageUtils imageUtils = new ImageUtils(targetUi, config); - DownloadImage downloadImage = new DownloadImage(targetUi, imageUtils); - GetPath getPath = new GetPath(targetUi, downloadImage); - return new GetDimens(targetUi, config, getPath); - } - - private void cancelUserAction() { - onView(withId(R.id.fab_camera)).perform(click()); - waitTime(); - - clickBottomMiddleScreen(); - rotateDevice(); - uiDevice.pressBack(); - - onView(withId(R.id.iv_image)).check(matches(new BoundedMatcher(ImageView.class) { - @Override - public void describeTo(Description description) { - imageDimens = new int[]{0, 0}; - description.appendText("has not drawable"); - } - - @Override - public boolean matchesSafely(ImageView imageView) { - return imageView.getDrawable() == null; - } - })); - } - - private void rotateDevice() { - try { - uiDevice.setOrientationLeft(); - waitTime(); - uiDevice.setOrientationNatural(); - waitTime(); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - - private void clickBottomMiddleScreen() { - int screenDimens[] = getScreenDimensions(); - int width = screenDimens[0]; - int height = screenDimens[1]; - - uiDevice.click(width / 2, height - 100); - waitTime(); - } - - private void clickTopRightScreen() { - int width = getScreenDimensions()[0]; - - uiDevice.click(width - 50, 100); - waitTime(); - } - - private void closeDrawer() { - int screenDimens[] = getScreenDimensions(); - int width = screenDimens[0]; - int height = screenDimens[1]; - - uiDevice.click(width - 50, height / 2); - waitTime(); - } - - private void clickImagesOnScreen(int imagesToPick) { - int screenDimens[] = getScreenDimensions(); - int width = screenDimens[0]; - int height = screenDimens[1]; - int y = 0; - - //closeDrawer(); - - for (int i = 0; i < imagesToPick; i++) { - int widthQuarter = width / 4; - int x = (i % 2 == 0) ? widthQuarter : widthQuarter * 3; - y += (i % 2 == 0) ? height / 4 : 0; - - if (imagesToPick == 1) uiDevice.click(x, y); - else uiDevice.swipe(x, y, x, y, 500); - } - waitTime(); - } - - private int[] getScreenDimensions() { - WindowManager wm = (WindowManager) InstrumentationRegistry.getTargetContext().getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - Point size = new Point(); - display.getSize(size); - return new int[]{size.x, size.y}; - } - - private void waitTime() { - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/DeviceConfig.java b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/DeviceConfig.java new file mode 100644 index 0000000..a075c30 --- /dev/null +++ b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/DeviceConfig.java @@ -0,0 +1,37 @@ +package com.miguelbcr.ui.rx_paparazzo2.sample; + + +public abstract class DeviceConfig { + + public static final DeviceConfig GOOGLE_NEXUS = new DeviceConfig() { + @Override + int getMultipleImageConfirmOffset() { + return 100; + } + }; + + public static final DeviceConfig SAMSUNG_GALAXY = new DeviceConfig() { + @Override + int getMultipleImageConfirmOffset() { + return 250; + } + }; + + public static DeviceConfig CURRENT = GOOGLE_NEXUS; + public static double WAIT_TIME_FUDGE_FACTOR = 1.0; + + abstract int getMultipleImageConfirmOffset(); + + public long getShortWaitTime() { + double waitTime = DeviceConfig.WAIT_TIME_FUDGE_FACTOR * 100; + + return (long) waitTime; + } + + public long getLongWaitTime() { + double waitTime = DeviceConfig.WAIT_TIME_FUDGE_FACTOR * 500; + + return (long) waitTime; + } + +} diff --git a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/DimensionMatcher.java b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/DimensionMatcher.java new file mode 100644 index 0000000..7c16ba5 --- /dev/null +++ b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/DimensionMatcher.java @@ -0,0 +1,108 @@ +package com.miguelbcr.ui.rx_paparazzo2.sample; + +import android.app.Activity; +import android.graphics.BitmapFactory; +import android.util.DisplayMetrics; + +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.CustomMaxSize; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.OriginalSize; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.ScreenSize; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; +import com.miguelbcr.ui.rx_paparazzo2.interactors.Dimensions; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import static org.hamcrest.Matchers.allOf; + +class DimensionMatcher extends BaseMatcher { + + private enum Dimension { + WIDTH, HEIGHT; + } + + private enum Match { + EQUALS, LESS_THAN_EQUAL, GREATER_THAN; + } + + private Dimension dimension; + private Match match; + private int expected; + + public DimensionMatcher(Dimension dimension, Match match, int expected) { + this.dimension = dimension; + this.match = match; + this.expected = expected; + } + + private int getDimension(FileData fileData) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(fileData.getFile().getAbsolutePath(), options); + + switch (dimension) { + case WIDTH: + return options.outWidth; + + case HEIGHT: + return options.outHeight; + + default: + throw new IllegalStateException("Unknown dimension " + dimension); + } + } + + @Override + public boolean matches(Object item) { + FileData fileData = (FileData) item; + + int dimension = getDimension(fileData); + switch (match) { + case EQUALS: + return dimension == expected; + + case LESS_THAN_EQUAL: + return dimension <= expected; + + case GREATER_THAN: + return dimension > expected; + + } + return false; + } + + @Override + public void describeTo(Description description) { + // TODO + } + + public static Matcher fromSize(Activity activity, Size size, Dimensions originalSize) { + if (size instanceof OriginalSize) { + DimensionMatcher widthMatcher = new DimensionMatcher(Dimension.WIDTH, Match.EQUALS, originalSize.getWidth()); + DimensionMatcher heightMatcher = new DimensionMatcher(Dimension.HEIGHT, Match.EQUALS, originalSize.getHeight()); + + return allOf(widthMatcher, heightMatcher); + } else if (size instanceof CustomMaxSize) { + CustomMaxSize maxSize = (CustomMaxSize) size; + int maxDimension = maxSize.getMaxImageSize(); + DimensionMatcher widthMatcher = new DimensionMatcher(Dimension.WIDTH, Match.LESS_THAN_EQUAL, maxDimension); + DimensionMatcher heightMatcher = new DimensionMatcher(Dimension.HEIGHT, Match.LESS_THAN_EQUAL, maxDimension); + + return allOf(widthMatcher, heightMatcher); + } else if (size instanceof ScreenSize) { + DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); + DimensionMatcher widthMatcher = new DimensionMatcher(Dimension.WIDTH, Match.LESS_THAN_EQUAL, metrics.widthPixels); + DimensionMatcher heightMatcher = new DimensionMatcher(Dimension.HEIGHT, Match.LESS_THAN_EQUAL, metrics.heightPixels); + + return allOf(widthMatcher, heightMatcher); + } else { + DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); + DimensionMatcher widthMatcher = new DimensionMatcher(Dimension.WIDTH, Match.LESS_THAN_EQUAL, metrics.widthPixels / 8); + DimensionMatcher heightMatcher = new DimensionMatcher(Dimension.HEIGHT, Match.LESS_THAN_EQUAL, metrics.heightPixels / 8); + + return allOf(widthMatcher, heightMatcher); + } + } +} diff --git a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/IsImageViewMatcher.java b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/IsImageViewMatcher.java new file mode 100644 index 0000000..2cf9a80 --- /dev/null +++ b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/IsImageViewMatcher.java @@ -0,0 +1,24 @@ +package com.miguelbcr.ui.rx_paparazzo2.sample; + +import android.support.test.espresso.matcher.BoundedMatcher; +import android.view.View; +import android.widget.ImageView; + +import org.hamcrest.Description; + +class IsImageViewMatcher extends BoundedMatcher { + + public IsImageViewMatcher() { + super(ImageView.class); + } + + @Override + public void describeTo(Description description) { + description.appendText("has drawable"); + } + + @Override + public boolean matchesSafely(ImageView imageView) { + return imageView.getDrawable() != null; + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/UiActions.java b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/UiActions.java new file mode 100644 index 0000000..3fea2ac --- /dev/null +++ b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/UiActions.java @@ -0,0 +1,225 @@ +package com.miguelbcr.ui.rx_paparazzo2.sample; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Point; +import android.os.RemoteException; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.contrib.RecyclerViewActions; +import android.support.test.uiautomator.UiDevice; +import android.view.Display; +import android.view.View; +import android.view.WindowManager; + +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; +import com.miguelbcr.ui.rx_paparazzo2.interactors.Dimensions; +import com.miguelbcr.ui.rx_paparazzo2.sample.activities.Testable; + +import org.hamcrest.Matcher; + +import java.io.File; +import java.util.List; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static com.miguelbcr.ui.rx_paparazzo2.sample.recyclerview.RecyclerViewUtils.withRecyclerView; +import static org.junit.Assert.assertNotNull; + +class UiActions { + + private UiDevice uiDevice; + + void setUiDevice(UiDevice uiDevice) { + this.uiDevice = uiDevice; + } + + void takePhoto(boolean crop) { + if (crop) { + onView(withId(R.id.fab_camera_crop)).perform(click()); + } else { + onView(withId(R.id.fab_camera)).perform(click()); + } + + waitTimeLong(); + + clickBottomMiddleScreen(); + rotateDevice(); + clickBottomMiddleScreen(); + + if (crop) { + rotateDevice(); + clickTopRightScreen(); + } + + waitTimeLong(); + + checkImageDimensions(0); + } + + void pickUpPhoto(boolean onlyOne) { + int imagesToPick = onlyOne ? 1 : 2; + + if (onlyOne) { + onView(withId(R.id.fab_pickup_image)).perform(click()); + } else { + onView(withId(R.id.fab_pickup_images)).perform(click()); + } + + waitTimeLong(); + + clickImagesOnScreen(imagesToPick); + + // Open selected images + if (!onlyOne) { + clickTopRightScreen(DeviceConfig.CURRENT.getMultipleImageConfirmOffset()); + } + + waitTimeLong(); + + // Close crop screen/s + for (int i = 0; i < imagesToPick; i++) { + clickTopRightScreen(); + waitTimeLong(); + } + + for (int i = 0; i < imagesToPick; i++) { + checkImageDimensions(i); + } + } + + + private void checkImageDimensions(int index) { + onView(withId(R.id.rv_images)).perform(RecyclerViewActions.scrollToPosition(index)); + + waitTimeLong(); + + Matcher itemAtPosition = withRecyclerView(R.id.rv_images).atPositionOnView(index, R.id.iv_image); + + IsImageViewMatcher isImageViewMatcher = new IsImageViewMatcher(); + onView(itemAtPosition).check(matches(isImageViewMatcher)); + + checkDimensions(); + } + + private void checkDimensions() { + Testable testable = getTestable(); + List fileDatas = testable.getFileDatas(); + + assertNotNull(fileDatas); + for (FileData fileData : fileDatas) { + assertNotNull(fileData); + + File file = fileData.getFile(); + assertNotNull(file); + + Size expectedSize = testable.getSize(); + Dimensions originalDimensions = fileData.getOriginalDimensions(); + DimensionMatcher.fromSize(getActivity(), expectedSize, originalDimensions).matches(fileData); + } + } + + private Testable getTestable() { + Activity activity = getActivity(); + if (activity instanceof Testable) { + return ((Testable) activity); + } + + throw new IllegalStateException("Expected Activity to implement Testable"); + } + + private Activity getActivity() { + return ((SampleApplication) InstrumentationRegistry.getTargetContext().getApplicationContext()).getLiveActivity(); + } + + void cancelUserAction() { + onView(withId(R.id.fab_camera)).perform(click()); + waitTime(); + + clickBottomMiddleScreen(); + rotateDevice(); + uiDevice.pressBack(); + + withRecyclerView(R.id.rv_images).isEmpty(); + + } + + private void rotateDevice() { + try { + uiDevice.setOrientationLeft(); + waitTimeLong(); + uiDevice.setOrientationNatural(); + waitTimeLong(); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + private void clickBottomMiddleScreen() { + int screenDimens[] = getScreenDimensions(); + int width = screenDimens[0]; + int height = screenDimens[1]; + + uiDevice.click(width / 2, height - 100); + waitTimeLong(); + } + + private void clickTopRightScreen() { + clickTopRightScreen(100); + } + + private void clickTopRightScreen(int offset) { + int width = getScreenDimensions()[0]; + + uiDevice.click(width - offset, 150); + waitTime(); + } + + private void clickImagesOnScreen(int imagesToPick) { + int screenDimens[] = getScreenDimensions(); + int width = screenDimens[0]; + int height = screenDimens[1]; + int y = 0; + + for (int i = 0; i < imagesToPick; i++) { + int widthQuarter = width / 4; + int x = (i % 2 == 0) ? widthQuarter : widthQuarter * 3; + y += (i % 2 == 0) ? height / 4 : 0; + + if (imagesToPick == 1) { + uiDevice.click(x, y); + } else { + uiDevice.swipe(x, y, x, y, 500); + } + + waitTimeLong(); + } + } + + private int[] getScreenDimensions() { + WindowManager wm = (WindowManager) InstrumentationRegistry.getTargetContext().getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + + return new int[]{size.x, size.y}; + } + + private void waitTime() { + waitTime(DeviceConfig.CURRENT.getShortWaitTime()); + } + + private void waitTimeLong() { + waitTime(DeviceConfig.CURRENT.getLongWaitTime()); + } + + private void waitTime(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/recyclerview/RecyclerViewMatcher.java b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/recyclerview/RecyclerViewMatcher.java index 2f9b01b..80be5d5 100644 --- a/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/recyclerview/RecyclerViewMatcher.java +++ b/app/src/androidTest/java/com/miguelbcr/ui/rx_paparazzo2/sample/recyclerview/RecyclerViewMatcher.java @@ -2,7 +2,9 @@ import android.content.res.Resources; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.View; + import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; @@ -18,49 +20,103 @@ public Matcher atPosition(final int position) { return atPositionOnView(position, -1); } + public Matcher isEmpty() { + return new TypeSafeMatcher() { + Resources resources = null; + + public void describeTo(Description description) { + String idDescription = getResourceName(resources, recyclerViewId); + description.appendText("Empty recycler view with id: " + idDescription); + } + + public boolean matchesSafely(View view) { + this.resources = view.getResources(); + + RecyclerView recyclerView = (RecyclerView) view.getRootView().findViewById(recyclerViewId); + if (!(recyclerView != null && recyclerView.getId() == recyclerViewId)) { + Log.i("MATCHER", "Recycler view missing"); + + return false; + } + + int childCount = recyclerView.getAdapter().getItemCount(); + if (childCount > 0) { + Log.i("MATCHER", "Recycler view only has " + childCount + " items, expected it was empty"); + + return false; + } + + return true; + } + }; + } + + private String getResourceName(Resources resources, int id) { + if (resources == null) { + return String.valueOf(id); + } + try { + return resources.getResourceName(id); + } catch (Resources.NotFoundException var4) { + return String.format("%s (resource name not found)", id); + } + } + public Matcher atPositionOnView(final int position, final int targetViewId) { return new TypeSafeMatcher() { Resources resources = null; - View childView; public void describeTo(Description description) { - String idDescription = Integer.toString(recyclerViewId); - if (this.resources != null) { - try { - idDescription = this.resources.getResourceName(recyclerViewId); - } catch (Resources.NotFoundException var4) { - idDescription = String.format("%s (resource name not found)", - new Object[] { Integer.valueOf - (recyclerViewId) }); - } + if (targetViewId != -1) { + String targetDescription = getResourceName(resources, targetViewId); + description.appendText("child with id: " + targetDescription + " of "); } - description.appendText("with id: " + idDescription); + String idDescription = getResourceName(resources, recyclerViewId); + description.appendText("item at index #" + position + " in view with id: " + idDescription); } public boolean matchesSafely(View view) { this.resources = view.getResources(); - if (childView == null) { - RecyclerView recyclerView = - (RecyclerView) view.getRootView().findViewById(recyclerViewId); - if (recyclerView != null && recyclerView.getId() == recyclerViewId) { - childView = recyclerView.getChildAt(position); - } - else { - return false; - } + RecyclerView recyclerView = (RecyclerView) view.getRootView().findViewById(recyclerViewId); + if (!(recyclerView != null && recyclerView.getId() == recyclerViewId)) { + Log.i("MATCHER", "Recycler view missing"); + + return false; + } + + int childCount = recyclerView.getAdapter().getItemCount(); + if (position >= childCount) { + Log.i("MATCHER", "Recycler view only has " + childCount + " items, cannot get index " + position); + + return false; + } + + RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position); + if (viewHolder.itemView == null) { + Log.i("MATCHER", "Item view in view holder was null"); + + return false; } if (targetViewId == -1) { - return view == childView; - } else { - View targetView = childView.findViewById(targetViewId); - return view == targetView; + boolean isItemView = (view.equals(viewHolder.itemView)); + + View targetView = viewHolder.itemView; + Log.i("MATCHER", targetView.toString() + " == " + view + " = " + isItemView); + + return isItemView; } + View targetView = viewHolder.itemView.findViewById(targetViewId); + boolean isItemView = view.equals(targetView); + + Log.i("MATCHER", targetView.toString() + " == " + view + " = " + isItemView); + + return isItemView; } }; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d16def4..b9c758f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,22 +5,32 @@ - + + + + + - + diff --git a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/SampleApplication.java b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/SampleApplication.java index cc7297a..7f148eb 100644 --- a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/SampleApplication.java +++ b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/SampleApplication.java @@ -2,23 +2,42 @@ import android.app.Activity; import android.app.Application; +import android.content.Context; import android.support.annotation.Nullable; import com.miguelbcr.ui.rx_paparazzo2.RxPaparazzo; +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; /** * Created by miguel on 16/03/2016. */ public class SampleApplication extends Application { + private RefWatcher refWatcher; + @Override public void onCreate() { super.onCreate(); RxPaparazzo.register(this); AppCare.YesSir.takeCareOn(this); + + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + refWatcher = LeakCanary.install(this); } @Nullable public Activity getLiveActivity(){ return AppCare.YesSir.getLiveActivityOrNull(); } + + public static RefWatcher getRefWatcher(Context context) { + SampleApplication application = (SampleApplication) context.getApplicationContext(); + + return application.refWatcher; + } + } diff --git a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/HostActivitySampleFragment.java b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/HostActivitySampleFragment.java index 901a86f..d05fb8c 100644 --- a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/HostActivitySampleFragment.java +++ b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/HostActivitySampleFragment.java @@ -3,6 +3,7 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; import com.miguelbcr.ui.rx_paparazzo2.sample.R; import com.miguelbcr.ui.rx_paparazzo2.sample.fragments.SampleFragment; @@ -25,4 +26,9 @@ public class HostActivitySampleFragment extends AppCompatActivity implements Tes @Override public Size getSize() { return fragment.getSize(); } + + @Override + public List getFileDatas() { + return fragment.getFileDatas(); + } } diff --git a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/PickerUtil.java b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/PickerUtil.java new file mode 100644 index 0000000..a7c8a9c --- /dev/null +++ b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/PickerUtil.java @@ -0,0 +1,36 @@ +package com.miguelbcr.ui.rx_paparazzo2.sample.activities; + +import android.app.Activity; +import android.content.Context; +import android.widget.Toast; + +import com.miguelbcr.ui.rx_paparazzo2.RxPaparazzo; +import com.miguelbcr.ui.rx_paparazzo2.sample.R; + +public class PickerUtil { + + public static boolean checkResultCode(Context context, int code) { + if (code == RxPaparazzo.RESULT_DENIED_PERMISSION) { + showUserDidNotGrantPermissions(context); + } else if (code == RxPaparazzo.RESULT_DENIED_PERMISSION_NEVER_ASK) { + showUserDidNotGrantPermissionsNeverAsk(context); + } else if (code != Activity.RESULT_OK) { + showUserCanceled(context); + } + + return code == Activity.RESULT_OK; + } + + private static void showUserCanceled(Context context) { + Toast.makeText(context, context.getString(R.string.user_canceled), Toast.LENGTH_SHORT).show(); + } + + private static void showUserDidNotGrantPermissions(Context context) { + Toast.makeText(context, context.getString(R.string.user_did_not_grant_permissions), Toast.LENGTH_SHORT).show(); + } + + private static void showUserDidNotGrantPermissionsNeverAsk(Context context) { + Toast.makeText(context, context.getString(R.string.user_did_not_grant_permissions_never_ask), Toast.LENGTH_SHORT).show(); + } + +} diff --git a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/SampleActivity.java b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/SampleActivity.java index f2c8764..4aa7d78 100644 --- a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/SampleActivity.java +++ b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/SampleActivity.java @@ -9,9 +9,12 @@ import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; import com.miguelbcr.ui.rx_paparazzo2.RxPaparazzo; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.Response; import com.miguelbcr.ui.rx_paparazzo2.entities.size.CustomMaxSize; import com.miguelbcr.ui.rx_paparazzo2.entities.size.OriginalSize; import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; @@ -21,16 +24,21 @@ import com.squareup.picasso.Picasso; import com.yalantis.ucrop.UCrop; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; +import java.io.File; import java.util.ArrayList; import java.util.List; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + public class SampleActivity extends AppCompatActivity implements Testable { - private Toolbar toolbar; - private ImageView imageView; + private static final String STATE_FILES = "FILES"; + public static final int ONE_MEGABYTE_IN_BYTES = 1000000; + private RecyclerView recyclerView; - private List filesPaths; + private ArrayList fileDataList; private Size size; @Override @@ -38,9 +46,16 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sample_layout); configureToolbar(); + + fileDataList = new ArrayList<>(); + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(STATE_FILES)) { + List files = (List) savedInstanceState.getSerializable(STATE_FILES); + fileDataList.addAll(files); + } + } + initViews(); - filesPaths = new ArrayList<>(); - size = new OriginalSize(); } @Override @@ -48,79 +63,101 @@ public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putSerializable(STATE_FILES, fileDataList); + } + private void configureToolbar() { - toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setTitle(R.string.app_name); } private void initViews() { - imageView = (ImageView) findViewById(R.id.iv_image); - recyclerView = (RecyclerView) findViewById(R.id.rv_images); - - recyclerView.setHasFixedSize(true); LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + recyclerView = (RecyclerView) findViewById(R.id.rv_images); + recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(layoutManager); findViewById(R.id.fab_camera).setOnClickListener(v -> captureImage()); findViewById(R.id.fab_camera_crop).setOnClickListener(v -> captureImageWithCrop()); findViewById(R.id.fab_pickup_image).setOnClickListener(v -> pickupImage()); findViewById(R.id.fab_pickup_images).setOnClickListener(v -> pickupImages()); + findViewById(R.id.fab_pickup_file).setOnClickListener(v -> pickupFile()); + findViewById(R.id.fab_pickup_files).setOnClickListener(v -> pickupFiles()); + + loadImages(); } private void captureImage() { - size = new CustomMaxSize(512); - RxPaparazzo.takeImage(SampleActivity.this) - .size(size) - .usingCamera() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(response -> { - if (checkResultCode(response.resultCode())) { - response.targetUI().loadImage(response.data()); - } - }, throwable -> { - throwable.printStackTrace(); - Toast.makeText(getApplicationContext(), "ERROR " + throwable.getMessage(), Toast.LENGTH_SHORT).show(); - }); + CustomMaxSize size = new CustomMaxSize(512); + + Observable> takeOnePhoto = pickSingle(null, size) + .usingCamera(); + + processSingle(takeOnePhoto); } private void captureImageWithCrop() { UCrop.Options options = new UCrop.Options(); options.setToolbarColor(ContextCompat.getColor(SampleActivity.this, R.color.colorAccent)); + options.setToolbarTitle("Cropping single photo"); - size = new OriginalSize(); - RxPaparazzo.takeImage(SampleActivity.this) - .size(size) - .crop(options) - .usingCamera() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(response -> { - if (checkResultCode(response.resultCode())) { - response.targetUI().loadImage(response.data()); - } - }, throwable -> { - throwable.printStackTrace(); - Toast.makeText(getApplicationContext(), "ERROR " + throwable.getMessage(), Toast.LENGTH_SHORT).show(); - }); + OriginalSize size = new OriginalSize(); + Observable> takePhotoAndCrop = pickSingle(options, size) + .usingCamera(); + + processSingle(takePhotoAndCrop); } private void pickupImage() { UCrop.Options options = new UCrop.Options(); options.setToolbarColor(ContextCompat.getColor(SampleActivity.this, R.color.colorPrimaryDark)); + options.setToolbarTitle("Cropping single image"); - size = new CustomMaxSize(500); - RxPaparazzo.takeImage(SampleActivity.this) - .useInternalStorage() - .crop(options) - .size(size) - .usingGallery() + Observable> pickUsingGallery = pickSingle(options, new CustomMaxSize(500)) + .usingGallery(); + + processSingle(pickUsingGallery); + } + + private void pickupImages() { + Observable>> pickMultiple = pickMultiple(new SmallSize()) + .usingGallery(); + + processMultiple(pickMultiple); + } + + private void pickupFile() { + UCrop.Options options = new UCrop.Options(); + options.setToolbarColor(ContextCompat.getColor(SampleActivity.this, R.color.colorPrimaryDark)); + options.setToolbarTitle("Cropping single file"); + + Observable> pickUsingGallery = pickSingle(options, new CustomMaxSize(500)) + .usingFiles(); + + processSingle(pickUsingGallery); + } + + private void pickupFiles() { + Size size = new SmallSize(); + + Observable>> pickMultiple = pickMultiple(size) + .usingFiles(); + + processMultiple(pickMultiple); + } + + private void processSingle(Observable> pickUsingGallery) { + pickUsingGallery .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { - if (checkResultCode(response.resultCode())) { + if (PickerUtil.checkResultCode(SampleActivity.this, response.resultCode())) { response.targetUI().loadImage(response.data()); } }, throwable -> { @@ -129,20 +166,32 @@ private void pickupImage() { }); } - private void pickupImages() { - size = new SmallSize(); - RxPaparazzo.takeImages(SampleActivity.this) - .useInternalStorage() - .crop() + private RxPaparazzo.SingleSelectionBuilder pickSingle(UCrop.Options options, Size size) { + this.size = size; + + RxPaparazzo.SingleSelectionBuilder resized = RxPaparazzo.single(this) + .setMaximumFileSizeInBytes(ONE_MEGABYTE_IN_BYTES) .size(size) - .usingGallery() + .sendToMediaScanner(); + + if (options != null) { + resized.crop(options); + } + + return resized; + } + + private Disposable processMultiple(Observable>> pickMultiple) { + return pickMultiple .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { - if (checkResultCode(response.resultCode())) { - if (response.data().size() == 1) + if (PickerUtil.checkResultCode(SampleActivity.this, response.resultCode())) { + if (response.data().size() == 1) { response.targetUI().loadImage(response.data().get(0)); - else response.targetUI().loadImages(response.data()); + } else { + response.targetUI().loadImages(response.data()); + } } }, throwable -> { throwable.printStackTrace(); @@ -150,53 +199,52 @@ private void pickupImages() { }); } - private boolean checkResultCode(int code) { - if (code == RxPaparazzo.RESULT_DENIED_PERMISSION) { - showUserDidNotGrantPermissions(); - } else if (code == RxPaparazzo.RESULT_DENIED_PERMISSION_NEVER_ASK) { - showUserDidNotGrantPermissionsNeverAsk(); - } else if (code != RESULT_OK) { - showUserCanceled(); - } + private RxPaparazzo.MultipleSelectionBuilder pickMultiple(Size size) { + this.size = size; - return code == RESULT_OK; + return RxPaparazzo.multiple(this) + .setMaximumFileSizeInBytes(ONE_MEGABYTE_IN_BYTES) + .crop() + .sendToMediaScanner() + .size(size); } - private void loadImage(String filePath) { - filesPaths.clear(); - filesPaths.add(filePath); - imageView.setVisibility(View.VISIBLE); - recyclerView.setVisibility(View.GONE); - imageView.setImageDrawable(null); - recyclerView.setAdapter(null); + private void loadImage(FileData fileData) { + this.fileDataList = new ArrayList<>(); + this.fileDataList.add(fileData); - Picasso.with(getApplicationContext()).setLoggingEnabled(true); - Picasso.with(getApplicationContext()).invalidate("file://" + filePath); - Picasso.with(getApplicationContext()).load("file://" + filePath).into(imageView); + loadImages(); } - private void loadImages(List filesPaths) { - this.filesPaths = filesPaths; - imageView.setVisibility(View.GONE); - recyclerView.setVisibility(View.VISIBLE); - imageView.setImageDrawable(null); - recyclerView.setAdapter(new ImagesAdapter(filesPaths)); - } + private void loadImages(List fileDataList) { + this.fileDataList = new ArrayList<>(fileDataList); - private void showUserCanceled() { - Toast.makeText(this, getString(R.string.user_canceled), Toast.LENGTH_SHORT).show(); + loadImages(); } - private void showUserDidNotGrantPermissions() { - Toast.makeText(this, getString(R.string.user_did_not_grant_permissions), Toast.LENGTH_SHORT).show(); + private void loadImages() { + if (fileDataList == null || fileDataList.isEmpty()) { + return; + } + + recyclerView.setVisibility(View.VISIBLE); + recyclerView.setAdapter(new ImagesAdapter(fileDataList)); } - private void showUserDidNotGrantPermissionsNeverAsk() { - Toast.makeText(this, getString(R.string.user_did_not_grant_permissions_never_ask), Toast.LENGTH_SHORT).show(); + @Override + public List getFileDatas() { + return fileDataList; } @Override public List getFilePaths() { + List filesPaths = new ArrayList<>(); + for (FileData fileData : fileDataList) { + File file = fileData.getFile(); + if (file != null) { + filesPaths.add(file.getAbsolutePath()); + } + } return filesPaths; } diff --git a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/Testable.java b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/Testable.java index 767b972..0ec182d 100644 --- a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/Testable.java +++ b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/activities/Testable.java @@ -1,11 +1,14 @@ package com.miguelbcr.ui.rx_paparazzo2.sample.activities; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; import java.util.List; -public interface Testable { +public interface Testable { + List getFileDatas(); + List getFilePaths(); Size getSize(); diff --git a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/adapters/ImagesAdapter.java b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/adapters/ImagesAdapter.java index aff45a5..e289149 100644 --- a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/adapters/ImagesAdapter.java +++ b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/adapters/ImagesAdapter.java @@ -1,23 +1,25 @@ package com.miguelbcr.ui.rx_paparazzo2.sample.adapters; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.AppCompatDrawableManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.TextView; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.sample.R; import com.squareup.picasso.Picasso; import java.io.File; import java.util.List; -/** - * Created by miguel on 16/03/2016. - */public class ImagesAdapter extends RecyclerView.Adapter{ - private List urlImages; +public class ImagesAdapter extends RecyclerView.Adapter { + private List urlImages; - public ImagesAdapter(List urlImages) { + public ImagesAdapter(List urlImages) { this.urlImages = urlImages; } @@ -37,16 +39,32 @@ public int getItemCount() { return urlImages == null ? 0 : urlImages.size(); } - protected static class ViewHolder extends RecyclerView.ViewHolder { + static class ViewHolder extends RecyclerView.ViewHolder { ImageView imageView; + TextView filenameView; ViewHolder(View itemView) { super(itemView); imageView = (ImageView)itemView.findViewById(R.id.iv_image); + filenameView = (TextView)itemView.findViewById(R.id.iv_filename); } - public void bind(String imageUrl) { - Picasso.with(imageView.getContext()).load(new File(imageUrl)).into(imageView); + void bind(FileData fileData) { + filenameView.setText(fileData.describe()); + File file = fileData.getFile(); + if (file != null && file.exists()) { + Picasso.with(imageView.getContext()) + .load(file) + .error(R.drawable.ic_description_black_48px) + .into(imageView); + } else { + if (fileData.isExceededMaximumFileSize()) { + filenameView.setText("MAXIMUM FILESIZE EXCEEDED"); + } + + Drawable drawable = AppCompatDrawableManager.get().getDrawable(imageView.getContext(), R.drawable.ic_description_black_48px); + imageView.setImageDrawable(drawable); + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/fragments/SampleFragment.java b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/fragments/SampleFragment.java index 5a6369d..b4d46da 100644 --- a/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/fragments/SampleFragment.java +++ b/app/src/main/java/com/miguelbcr/ui/rx_paparazzo2/sample/fragments/SampleFragment.java @@ -1,6 +1,5 @@ package com.miguelbcr.ui.rx_paparazzo2.sample.fragments; -import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; @@ -10,175 +9,237 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; import com.miguelbcr.ui.rx_paparazzo2.RxPaparazzo; -import com.miguelbcr.ui.rx_paparazzo2.entities.Options; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.Response; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.CustomMaxSize; import com.miguelbcr.ui.rx_paparazzo2.entities.size.OriginalSize; import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; import com.miguelbcr.ui.rx_paparazzo2.entities.size.SmallSize; import com.miguelbcr.ui.rx_paparazzo2.sample.R; +import com.miguelbcr.ui.rx_paparazzo2.sample.activities.PickerUtil; import com.miguelbcr.ui.rx_paparazzo2.sample.activities.Testable; import com.miguelbcr.ui.rx_paparazzo2.sample.adapters.ImagesAdapter; import com.squareup.picasso.Picasso; import com.yalantis.ucrop.UCrop; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.io.File; import java.util.ArrayList; import java.util.List; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + public class SampleFragment extends Fragment implements Testable { - private ImageView imageView; + private static final String STATE_FILES = "FILES"; + private RecyclerView recyclerView; - private List filesPaths; + private ArrayList fileDataList; private Size size; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.sample_layout, container, false); - return view; + return inflater.inflate(R.layout.sample_layout, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - initViews(); - filesPaths = new ArrayList<>(); + + fileDataList = new ArrayList<>(); + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(STATE_FILES)) { + List files = (List) savedInstanceState.getSerializable(STATE_FILES); + fileDataList.addAll(files); + } + } + size = new OriginalSize(); + + initViews(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putSerializable(STATE_FILES, fileDataList); } private void initViews() { - imageView = (ImageView) getView().findViewById(R.id.iv_image); - recyclerView = (RecyclerView) getView().findViewById(R.id.rv_images); + View view = getView(); - recyclerView.setHasFixedSize(true); LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + recyclerView = (RecyclerView) view.findViewById(R.id.rv_images); + recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(layoutManager); - getView().findViewById(R.id.fab_camera).setOnClickListener(v -> captureImage()); - getView().findViewById(R.id.fab_camera_crop).setOnClickListener(v -> captureImageWithCrop()); - getView().findViewById(R.id.fab_pickup_image).setOnClickListener(v -> pickupImage()); - getView().findViewById(R.id.fab_pickup_images).setOnClickListener(v -> pickupImages()); + view.findViewById(R.id.fab_camera).setOnClickListener(v -> captureImage()); + view.findViewById(R.id.fab_camera_crop).setOnClickListener(v -> captureImageWithCrop()); + view.findViewById(R.id.fab_pickup_image).setOnClickListener(v -> pickupImage()); + view.findViewById(R.id.fab_pickup_images).setOnClickListener(v -> pickupImages()); + view.findViewById(R.id.fab_pickup_file).setOnClickListener(v -> pickupFile()); + view.findViewById(R.id.fab_pickup_files).setOnClickListener(v -> pickupFiles()); + + loadImages(); } private void captureImage() { - size = new SmallSize(); - RxPaparazzo.takeImage(this) - .size(size) - .usingCamera() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(response -> { - if (checkResultCode(response.resultCode())) { - response.targetUI().loadImage(response.data()); - } - }); + SmallSize size = new SmallSize(); + + Observable> takeOnePhoto = pickSingle(null, size) + .usingCamera(); + + processSingle(takeOnePhoto); } private void captureImageWithCrop() { - Options options = new Options(); - options.setToolbarColor(ContextCompat.getColor(getActivity(), R.color.colorAccent)); - options.setAspectRatio(25, 75); + UCrop.Options options = new UCrop.Options(); + options.setToolbarColor(ContextCompat.getColor(getContext(), R.color.colorAccent)); + options.setToolbarTitle("Cropping single photo"); + options.withAspectRatio(25, 75); - size = new OriginalSize(); - RxPaparazzo.takeImage(this) - .size(size) - .crop(options) - .usingCamera() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(response -> { - if (checkResultCode(response.resultCode())) { - response.targetUI().loadImage(response.data()); - } - }); + OriginalSize size = new OriginalSize(); + + Observable> takePhotoAndCrop = pickSingle(options, size) + .usingCamera(); + + processSingle(takePhotoAndCrop); } private void pickupImage() { UCrop.Options options = new UCrop.Options(); - options.setToolbarColor(ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark)); - - size = new SmallSize(); - RxPaparazzo.takeImage(this) - .useInternalStorage() - .crop(options) - .size(size) - .usingGallery() + options.setToolbarColor(ContextCompat.getColor(getContext(), R.color.colorPrimaryDark)); + options.setToolbarTitle("Cropping single image"); + + Observable> pickUsingGallery = pickSingle(options, new SmallSize()) + .usingGallery(); + + processSingle(pickUsingGallery); + } + + private void pickupImages() { + Observable>> pickMultiple = pickMultiple(new SmallSize()) + .usingGallery(); + + processMultiple(pickMultiple); + } + + private void pickupFile() { + UCrop.Options options = new UCrop.Options(); + options.setToolbarColor(ContextCompat.getColor(getContext(), R.color.colorPrimaryDark)); + options.setToolbarTitle("Cropping single file"); + + Observable> pickUsingGallery = pickSingle(options, new CustomMaxSize(500)) + .usingFiles(); + + processSingle(pickUsingGallery); + } + + private void pickupFiles() { + Size size = new SmallSize(); + + Observable>> pickMultiple = pickMultiple(size) + .usingFiles(); + + processMultiple(pickMultiple); + } + + private void processSingle(Observable> pickUsingGallery) { + pickUsingGallery .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { - if (checkResultCode(response.resultCode())) { + if (PickerUtil.checkResultCode(getContext(), response.resultCode())) { response.targetUI().loadImage(response.data()); } + }, throwable -> { + throwable.printStackTrace(); + Toast.makeText(getContext(), "ERROR " + throwable.getMessage(), Toast.LENGTH_SHORT).show(); }); } - private void pickupImages() { - size = new SmallSize(); - RxPaparazzo.takeImages(this) - .useInternalStorage() - .crop() - .size(size) - .usingGallery() + private RxPaparazzo.SingleSelectionBuilder pickSingle(UCrop.Options options, Size size) { + this.size = size; + + RxPaparazzo.SingleSelectionBuilder resized = RxPaparazzo.single(this) + .sendToMediaScanner() + .size(size); + + if (options != null) { + resized.crop(options); + } + + return resized; + } + + private Disposable processMultiple(Observable>> pickMultiple) { + return pickMultiple .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { - if (checkResultCode(response.resultCode())) { - if (response.data().size() == 1) + if (PickerUtil.checkResultCode(getContext(), response.resultCode())) { + if (response.data().size() == 1) { response.targetUI().loadImage(response.data().get(0)); - else response.targetUI().loadImages(response.data()); + } else { + response.targetUI().loadImages(response.data()); + } } + }, throwable -> { + throwable.printStackTrace(); + Toast.makeText(getContext(), "ERROR " + throwable.getMessage(), Toast.LENGTH_SHORT).show(); }); } - private boolean checkResultCode(int code) { - if (code == RxPaparazzo.RESULT_DENIED_PERMISSION) { - showUserDidNotGrantPermissions(); - } else if (code == RxPaparazzo.RESULT_DENIED_PERMISSION_NEVER_ASK) { - showUserDidNotGrantPermissionsNeverAsk(); - } else if (code != Activity.RESULT_OK) { - showUserCanceled(); - } + private RxPaparazzo.MultipleSelectionBuilder pickMultiple(Size size) { + this.size = size; - return code == Activity.RESULT_OK; + return RxPaparazzo.multiple(this) + .sendToMediaScanner() + .crop() + .size(size); } - private void loadImage(String filePath) { - filesPaths.clear(); - filesPaths.add(filePath); - imageView.setVisibility(View.VISIBLE); - recyclerView.setVisibility(View.GONE); - File imageFile = new File(filePath); - Picasso.with(getActivity()).setLoggingEnabled(true); - Picasso.with(getActivity()).invalidate(new File(filePath)); - Picasso.with(getActivity()).load(imageFile).into(imageView); - } + private void loadImage(FileData fileData) { + this.fileDataList = new ArrayList<>(); + this.fileDataList.add(fileData); - private void loadImages(List filesPaths) { - this.filesPaths = filesPaths; - imageView.setVisibility(View.GONE); - recyclerView.setVisibility(View.VISIBLE); - recyclerView.setAdapter(new ImagesAdapter(filesPaths)); + loadImages(); } - private void showUserCanceled() { - Toast.makeText(getActivity(), getString(R.string.user_canceled), Toast.LENGTH_SHORT).show(); + private void loadImages() { + this.fileDataList = new ArrayList<>(fileDataList); + + loadImages(fileDataList); } - private void showUserDidNotGrantPermissions() { - Toast.makeText(getActivity(), getString(R.string.user_did_not_grant_permissions), Toast.LENGTH_SHORT).show(); + private void loadImages(List fileDataList) { + if (fileDataList == null || fileDataList.isEmpty()) { + return; + } + + recyclerView.setVisibility(View.VISIBLE); + recyclerView.setAdapter(new ImagesAdapter(fileDataList)); } - private void showUserDidNotGrantPermissionsNeverAsk() { - Toast.makeText(getActivity(), getString(R.string.user_did_not_grant_permissions_never_ask), Toast.LENGTH_SHORT).show(); + @Override + public List getFileDatas() { + return fileDataList; } @Override public List getFilePaths() { + List filesPaths = new ArrayList<>(); + for (FileData fileData : fileDataList) { + filesPaths.add(fileData.getFile().getAbsolutePath()); + } + return filesPaths; } diff --git a/app/src/main/res/drawable/ic_description_black_48px.xml b/app/src/main/res/drawable/ic_description_black_48px.xml new file mode 100644 index 0000000..0b1281c --- /dev/null +++ b/app/src/main/res/drawable/ic_description_black_48px.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_images.xml b/app/src/main/res/layout/item_images.xml index efc4df6..1118e1e 100644 --- a/app/src/main/res/layout/item_images.xml +++ b/app/src/main/res/layout/item_images.xml @@ -2,15 +2,25 @@ + android:orientation="vertical" + android:paddingRight="10dp" + android:paddingBottom="10dp"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/sample_layout.xml b/app/src/main/res/layout/sample_layout.xml index ac94a93..050695d 100644 --- a/app/src/main/res/layout/sample_layout.xml +++ b/app/src/main/res/layout/sample_layout.xml @@ -21,74 +21,112 @@ - - - - - + + + + + + + + - - + + + + + + + + - + android:orientation="horizontal" + > - + + + + + + + - RxPaparazzo-2 + RxPaparazzo Settings User canceled action User has not accepted permissions diff --git a/rx_paparazzo/src/main/res/xml/rx_paparazzo_file_paths.xml b/app/src/main/res/xml/rx_paparazzo_file_paths.xml similarity index 100% rename from rx_paparazzo/src/main/res/xml/rx_paparazzo_file_paths.xml rename to app/src/main/res/xml/rx_paparazzo_file_paths.xml diff --git a/gradle.properties b/gradle.properties index 1d3591c..e4a345a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,5 @@ # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true +org.gradle.jvmargs=-Xmx1536M \ No newline at end of file diff --git a/rx_paparazzo/build.gradle b/rx_paparazzo/build.gradle index a76a0ba..c7cc537 100644 --- a/rx_paparazzo/build.gradle +++ b/rx_paparazzo/build.gradle @@ -68,3 +68,17 @@ artifacts { archives javadocJar } +install { + repositories.mavenInstaller { + pom { + project { + packaging 'aar' + name 'com.github.miguelbcr' + pom.groupId = 'com.github.miguelbcr' + pom.artifactId = 'RxPaparazzo' + pom.version = '2.x-agrimap' + } + } + } +} + diff --git a/rx_paparazzo/src/main/AndroidManifest.xml b/rx_paparazzo/src/main/AndroidManifest.xml index 40e5083..0b9ee14 100644 --- a/rx_paparazzo/src/main/AndroidManifest.xml +++ b/rx_paparazzo/src/main/AndroidManifest.xml @@ -9,16 +9,6 @@ android:label="@string/app_name" android:supportsRtl="true"> - - - - diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/RxPaparazzo.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/RxPaparazzo.java index 08211e3..add1cb2 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/RxPaparazzo.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/RxPaparazzo.java @@ -19,14 +19,19 @@ import android.app.Activity; import android.app.Application; import android.support.v4.app.Fragment; + import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.Response; import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; +import com.miguelbcr.ui.rx_paparazzo2.interactors.ImageUtils; import com.miguelbcr.ui.rx_paparazzo2.internal.di.ApplicationComponent; import com.miguelbcr.ui.rx_paparazzo2.internal.di.ApplicationModule; import com.yalantis.ucrop.UCrop; -import io.reactivex.Observable; + import java.util.List; + +import io.reactivex.Observable; import rx_activity_result2.RxActivityResult; public final class RxPaparazzo { @@ -37,146 +42,198 @@ public static void register(Application application) { RxActivityResult.register(application); } - public static BuilderImage takeImage(T activity) { - return new BuilderImage(activity); + public static SingleSelectionBuilder single(T activity) { + return new SingleSelectionBuilder(activity); + } + + public static SingleSelectionBuilder single(T fragment) { + return new SingleSelectionBuilder(fragment); } /** * Prior to API 18, only one image will be retrieved. */ - public static BuilderImages takeImages(T activity) { - return new BuilderImages(activity); - } - - public static BuilderImage takeImage(T fragment) { - return new BuilderImage(fragment); + public static MultipleSelectionBuilder multiple(T activity) { + return new MultipleSelectionBuilder(activity); } /** * Prior to API 18, only one image will be retrieved. */ - public static BuilderImages takeImages(T fragment) { - return new BuilderImages(fragment); + public static MultipleSelectionBuilder multiple(T fragment) { + return new MultipleSelectionBuilder(fragment); } - private abstract static class Builder { - protected final Config config; - protected final ApplicationComponent applicationComponent; + private abstract static class Builder> { + private final Config config; + private final ApplicationComponent applicationComponent; + private final B self; - public Builder(T ui) { + Builder(T ui) { + this.self = (B)this; this.config = new Config(); this.applicationComponent = ApplicationComponent.create(new ApplicationModule(config, ui)); } - } - /** - * Call it when just one image is required to retrieve. - */ - public static class BuilderImage extends Builder { + ApplicationComponent getApplicationComponent() { + return applicationComponent; + } - public BuilderImage(T ui) { - super(ui); + Config getConfig() { + return config; + } + + public B setMaximumFileSizeInBytes(long maximumFileSizeInBytes) { + this.config.setMaximumFileSize(maximumFileSizeInBytes); + return self; + } + + /** + * Sets this to the value of name attribute of {@link android.support.v4.content.FileProvider} in AndroidManifest.xml + */ + public B setFileProviderAuthority(String authority) { + this.config.setFileProviderAuthority(authority); + return self; + } + + /** + * Sets this to the path to use in the {@link android.support.v4.content.FileProvider} xml file + */ + public B setFileProviderPath(String authority) { + this.config.setFileProviderPath(authority); + return self; + } + + /** + * Limits the file which can be selected to those obey {@link android.content.Intent}.CATEGORY_OPENABLE. + */ + public B limitPickerToOpenableFilesOnly() { + this.config.setPickOpenableOnly(true); + return self; } /** * Calling it the images will be saved in internal storage, otherwise in public storage */ - public BuilderImage useInternalStorage() { - this.config.setUseInternalStorage(); - return this; + public B useInternalStorage() { + this.config.setUseInternalStorage(true); + return self; } /** - * Sets the size for the retrieved image. + * Sets the image dimensions for the retrieved image. * * @see Size */ - public BuilderImage size(Size size) { + public B size(Size size) { this.config.setSize(size); - return this; + return self; } /** - * Call it when crop option is required. + * Sets the mime type of the picker. */ - public BuilderImage crop() { + public B setMimeType(String mimeType) { + this.config.setPickMimeType(mimeType); + return self; + } + + /** + * Enables cropping of images. + */ + public B crop() { this.config.setCrop(); - return this; + return self; } /** - * Call it when crop option is required as such as configuring the options of the cropping + * Sets crop option is required as such as configuring the options of the cropping * action. */ - public BuilderImage crop(O options) { + public B crop(O options) { this.config.setCrop(options); - return this; + return self; } /** - * Use gallery to retrieve the image. + * Send result to media scanner */ - public Observable> usingGallery() { - return applicationComponent.gallery().pickImage(); + public B sendToMediaScanner() { + this.config.setSendToMediaScanner(true); + return self; } /** - * Use camera to retrieve the image. + * Use Android Storage Access Framework document picker */ - public Observable> usingCamera() { - return applicationComponent.camera().takePhoto(); + public B useDocumentPicker() { + this.config.setUseDocumentPicker(true); + return self; } + } /** - * Call it when multiple images are required to retrieve from gallery. + * Use when just one image is required. */ - public static class BuilderImages extends Builder { + public static class SingleSelectionBuilder extends Builder> { - public BuilderImages(T ui) { + SingleSelectionBuilder(T ui) { super(ui); } /** - * Calling it the images will be saved in internal storage, otherwise in public storage + * Use file picker to retrieve only images. */ - public BuilderImages useInternalStorage() { - this.config.setUseInternalStorage(); - return this; + public Observable> usingGallery() { + Config config = getConfig(); + config.setPickMimeType(ImageUtils.MIME_TYPE_IMAGE_WILDCARD); + config.setFailCropIfNotImage(true); + + return usingFiles(); } /** - * Sets the size for the retrieved image. - * - * @see Size + * Use camera to retrieve the image. */ - public BuilderImages size(Size size) { - this.config.setSize(size); - return this; + public Observable> usingCamera() { + getConfig().setFailCropIfNotImage(true); + + return getApplicationComponent().camera().takePhoto(); } /** - * Call it when crop option is required. + * Use file picker to retrieve the files. */ - public BuilderImages crop() { - this.config.setCrop(); - return this; + public Observable> usingFiles() { + return getApplicationComponent().files().pickFile(); + } + } + + /** + * Use when multiple images are required. + */ + public static class MultipleSelectionBuilder extends Builder> { + + MultipleSelectionBuilder(T ui) { + super(ui); } /** - * Call it when crop option is required as such as configuring the options of the cropping - * action. + * Use file picker to retrieve only images. */ - public BuilderImages crop(O options) { - this.config.setCrop(options); - return this; + public Observable>> usingGallery() { + getConfig().setPickMimeType(ImageUtils.MIME_TYPE_IMAGE_WILDCARD); + + return usingFiles(); } /** - * Call it when crop option is required. + * Use file picker to retrieve the files. */ - public Observable>> usingGallery() { - return applicationComponent.gallery().pickImages(); + public Observable>> usingFiles() { + return getApplicationComponent().files().pickFiles(); } + } } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/Config.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/Config.java index 1f7c75a..f272d96 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/Config.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/Config.java @@ -16,28 +16,58 @@ package com.miguelbcr.ui.rx_paparazzo2.entities; +import android.content.Context; + import com.miguelbcr.ui.rx_paparazzo2.entities.size.ScreenSize; import com.miguelbcr.ui.rx_paparazzo2.entities.size.Size; import com.yalantis.ucrop.UCrop; public class Config { + + private static final String DEFAULT_FILE_PROVIDER_PATH = "RxPaparazzo"; + private static final String DEFAULT_FILE_PROVIDER_AUTHORITIES_SUFFIX = "file_provider"; + private static final long NO_FILESIZE_LIMIT = Long.MAX_VALUE; + + private UCrop.Options options; + + private long maximumFileSize; private Size size; private boolean doCrop; - private UCrop.Options options; + private boolean failCropIfNotImage; private boolean useInternalStorage; + private String pickMimeType; + private boolean pickOpenableOnly; + private boolean useDocumentPicker; + private boolean sendToMediaScanner; + + private String fileProviderAuthority; + private String fileProviderDirectory; + public Config() { this.size = new ScreenSize(); this.doCrop = false; this.useInternalStorage = false; + this.useDocumentPicker = false; + this.pickOpenableOnly = false; + this.pickMimeType = null; + this.sendToMediaScanner = false; + this.failCropIfNotImage = false; + this.fileProviderAuthority = null; + this.fileProviderDirectory = null; + this.maximumFileSize = NO_FILESIZE_LIMIT; } - public Size getSize() { - return size; + public void setMaximumFileSize(long maximumFileSize) { + this.maximumFileSize = maximumFileSize; } - public boolean doCrop() { - return doCrop; + public long getMaximumFileSize() { + return maximumFileSize; + } + + public Size getSize() { + return size; } public void setCrop(UCrop.Options options) { @@ -58,11 +88,84 @@ public void setSize(Size size) { this.size = size; } - public boolean useInternalStorage() { + public boolean isUseInternalStorage() { return useInternalStorage; } - public void setUseInternalStorage() { - this.useInternalStorage = true; + public void setUseInternalStorage(boolean useInternalStorage) { + this.useInternalStorage = useInternalStorage; + } + + public void setUseDocumentPicker(boolean useDocumentPicker) { + this.useDocumentPicker = useDocumentPicker; + } + + public boolean isUseDocumentPicker() { + return useDocumentPicker; + } + + public void setPickMimeType(String pickMimeType) { + this.pickMimeType = pickMimeType; + } + + public String getMimeType(String defaultMimeType) { + if (this.pickMimeType == null) { + return defaultMimeType; + } + + return pickMimeType; + } + + public void setPickOpenableOnly(boolean pickOpenableOnly) { + this.pickOpenableOnly = pickOpenableOnly; + } + + public boolean isPickOpenableOnly() { + return pickOpenableOnly; + } + + public void setSendToMediaScanner(boolean sendToMediaScanner) { + this.sendToMediaScanner = sendToMediaScanner; + } + + public boolean isSendToMediaScanner() { + return sendToMediaScanner; } + + public void setFailCropIfNotImage(boolean failCropIfNotImage) { + this.failCropIfNotImage = failCropIfNotImage; + } + + public boolean isFailCropIfNotImage() { + return failCropIfNotImage; + } + + public boolean isDoCrop() { + return doCrop; + } + + public void setFileProviderAuthority(String fileProviderAuthority) { + this.fileProviderAuthority = fileProviderAuthority; + } + + public String getFileProviderAuthority(Context context) { + if (fileProviderAuthority == null) { + return context.getPackageName() + "." + DEFAULT_FILE_PROVIDER_AUTHORITIES_SUFFIX; + } + + return fileProviderAuthority; + } + + public void setFileProviderPath(String fileProviderDirectory) { + this.fileProviderDirectory = fileProviderDirectory; + } + + public String getFileProviderDirectory() { + if (fileProviderDirectory == null) { + return DEFAULT_FILE_PROVIDER_PATH; + } + + return fileProviderDirectory; + } + } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/FileData.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/FileData.java new file mode 100644 index 0000000..62241ff --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/FileData.java @@ -0,0 +1,121 @@ +package com.miguelbcr.ui.rx_paparazzo2.entities; + +import android.util.Log; + +import com.miguelbcr.ui.rx_paparazzo2.interactors.Dimensions; + +import java.io.File; +import java.io.Serializable; + +public class FileData implements Serializable { + + private static final String FILENAME_MIMETYPE = "%s (%s)"; + private static final String FILENAME_MIMETYPE_TITLE = "%s (%s) - %s"; + private static final String FILENAME_TRANSIENT_MIMETYPE_TITLE = "%s %s (%s) - %s"; + + private File file; + private String filename; + private String mimeType; + private String title; + private boolean transientFile; + private boolean exceededMaximumFileSize; + private Dimensions originalDimensions; + + public static FileData toFileDataDeleteSourceFileIfTransient(FileData source, File file, boolean transientFile, String mimeType) { + deleteSourceFile(source); + + return new FileData(source, file, transientFile, mimeType); + } + + public static void deleteSourceFile(FileData fileData) { + if (fileData.isTransientFile()) { + File file = fileData.getFile(); + if (file != null && file.exists()) { + try { + Log.i(FileData.class.getSimpleName(), String.format("Removing source file '%s'", file.getAbsolutePath())); + if (!file.delete()) { + // silently fail delete + } + } catch (Exception e) { + Log.i(FileData.class.getSimpleName(), String.format("Could not remove source file '%s'", file.getAbsolutePath()), e); + } + } + } + } + + public static FileData exceededMaximumFileSize(FileData source) { + return new FileData(null, true, source.getFilename(), source.getMimeType(), source.getTitle(), source.getOriginalDimensions(), true); + } + + public FileData(FileData source, Dimensions dimensions) { + this(source.getFile(), source.isTransientFile(), source.getFilename(), source.getMimeType(), source.getTitle(), dimensions, source.isExceededMaximumFileSize()); + } + + public FileData(FileData source, File file, boolean transientFile, String mimeType) { + this(file, transientFile, source.getFilename(), mimeType, source.getTitle(), source.getOriginalDimensions(), source.isExceededMaximumFileSize()); + } + + public FileData(File file, boolean transientFile, String filename, String mimeType) { + this(file, transientFile, filename, mimeType, null, null, false); + } + + public FileData(File file, boolean transientFile, String filename, String mimeType, String title) { + this(file, transientFile, filename, mimeType, title, null, false); + } + +// public FileData(File file, boolean transientFile, String filename, String mimeType, String title, Dimensions dimensions) { +// this(file, transientFile, filename, mimeType, title, dimensions, false); +// } + + public FileData(File file, boolean transientFile, String filename, String mimeType, String title, Dimensions originalDimensions, boolean exceededMaximumFileSize) { + this.filename = filename; + this.transientFile = transientFile; + this.mimeType = mimeType; + this.file = file; + this.title = title; + this.exceededMaximumFileSize = exceededMaximumFileSize; + this.originalDimensions = originalDimensions; + } + + public Dimensions getOriginalDimensions() { + return originalDimensions; + } + + public File getFile() { + return file; + } + + public String getFilename() { + return filename; + } + + public String getMimeType() { + return mimeType; + } + + public String getTitle() { + return title; + } + + public boolean isTransientFile() { + return transientFile; + } + + public boolean isExceededMaximumFileSize() { + return exceededMaximumFileSize; + } + + public String describe() { + if (title == null) { + return String.format(FILENAME_MIMETYPE, filename, mimeType); + } + + return String.format(FILENAME_MIMETYPE_TITLE, filename, mimeType, title); + } + + @Override + public String toString() { + String transientDescription = transientFile ? "Transient" : "Not transient"; + return String.format(FILENAME_TRANSIENT_MIMETYPE_TITLE, transientDescription, filename, mimeType, title); + } +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/TargetUi.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/TargetUi.java index b8cc1c8..8a63996 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/TargetUi.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/entities/TargetUi.java @@ -32,7 +32,8 @@ public Activity activity() { return fragment() != null ? fragment().getActivity() : (Activity) ui; } - @Nullable public Fragment fragment() { + @Nullable + public Fragment fragment() { if (ui instanceof Fragment) { return (Fragment) ui; } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/Constants.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/Constants.java deleted file mode 100644 index b4ec995..0000000 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/Constants.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016 Miguel Garcia - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.miguelbcr.ui.rx_paparazzo2.interactors; - -class Constants { - static final String SUBDIR = "RxPaparazzo"; - static final String SHOOT_APPEND = "shoot.jpg"; - static final String CROP_APPEND = "cropped"; - static final String NO_CROP_APPEND = "no_cropped"; - static final String EXT_PNG = "png"; - /** - * The same name that is placed on the manifest on provivder tag - */ - static final String FILE_PROVIDER = "file_provider"; - static final String DATE_FORMAT = "ddMMyyyy_HHmmss_SSS"; - static final String LOCALE_EN = "en"; -} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/CropImage.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/CropImage.java index 8bb0258..11bf7b1 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/CropImage.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/CropImage.java @@ -18,112 +18,142 @@ import android.content.Intent; import android.net.Uri; + import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.Options; import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; import com.yalantis.ucrop.UCrop; + +import java.io.File; +import java.io.FileNotFoundException; + import io.reactivex.Observable; import io.reactivex.ObservableSource; -import io.reactivex.functions.BiFunction; import io.reactivex.functions.Function; -import java.io.File; -public final class CropImage extends UseCase { +public final class CropImage extends UseCase { + private static final String CROP_APPEND = "CROPPED-"; + private final Config config; private final StartIntent startIntent; - private final GetPath getPath; private final TargetUi targetUi; private final ImageUtils imageUtils; - private Uri uri; + private FileData fileData; - public CropImage(TargetUi targetUi, Config config, StartIntent startIntent, GetPath getPath, - ImageUtils imageUtils) { + public CropImage(TargetUi targetUi, Config config, StartIntent startIntent, ImageUtils imageUtils) { this.targetUi = targetUi; this.config = config; this.startIntent = startIntent; - this.getPath = getPath; this.imageUtils = imageUtils; } - public CropImage with(Uri uri) { - this.uri = uri; + public CropImage with(FileData fileData) { + this.fileData = fileData; return this; } - @Override public Observable react() { - if (config.doCrop()) { - return getIntent().flatMap(new Function>() { - @Override public ObservableSource apply(Intent intent) throws Exception { - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - return startIntent.with(intent).react().map(new Function() { - @Override public Uri apply(Intent intentResult) throws Exception { - return UCrop.getOutput(intentResult); - } - }); + @Override + public Observable react() { + if (config.isDoCrop()) { + + // bypass cropping if not image + if (!isImage()) { + if (config.isFailCropIfNotImage()) { + throw new IllegalArgumentException("Expected an image file, cannot perform image crop"); + } else { + return Observable.just(fileData); } - }); + } + + return cropImage(); } - return getOutputUriNoCrop(); + return Observable.just(fileData); } - private Observable getIntent() { - return Observable.zip(getInputUri(), getOutputUriCrop(), new BiFunction() { - @Override public Intent apply(Uri uri, Uri outputUri) throws Exception { - UCrop.Options options = config.getOptions(); - if (options == null) return UCrop.of(uri, outputUri).getIntent(targetUi.getContext()); + private Observable cropImage() { + final File outputFile = getOutputFile(); + Uri outputUri = Uri.fromFile(outputFile); + Observable intent = Observable.just(getIntent(outputUri)); - if (options instanceof Options) { - return getIntentWithOptions((Options) options, outputUri); - } else { - return UCrop.of(uri, outputUri) - .withOptions(config.getOptions()) - .getIntent(targetUi.getContext()); - } + return intent.flatMap(new Function>() { + @Override + public ObservableSource apply(Intent intent) throws Exception { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + return startIntent.with(intent).react() + .map(new Function() { + @Override + public Uri apply(Intent intentResult) throws Exception { + return UCrop.getOutput(intentResult); + } + }) + .flatMap(new Function>() { + @Override + public ObservableSource apply(Uri uri) throws Exception { + if (!outputFile.exists()) { + throw new FileNotFoundException(String.format("Cropped file not saved", outputFile.getAbsolutePath())); + } + + FileData result = FileData.toFileDataDeleteSourceFileIfTransient(fileData, outputFile, true, ImageUtils.MIME_TYPE_JPEG); + + return Observable.just(result); + } + }); } }); } + private boolean isImage() { + File file = fileData.getFile(); + + return imageUtils.isImage(file); + } + + private Intent getIntent(Uri outputUri) { + Uri inputUri = getInputUri(); + + UCrop.Options options = config.getOptions(); + if (options == null) { + return UCrop.of(inputUri, outputUri).getIntent(targetUi.getContext()); + } + + if (options instanceof Options) { + return getIntentWithOptions((Options) options, outputUri); + } else { + return UCrop.of(inputUri, outputUri) + .withOptions(config.getOptions()) + .getIntent(targetUi.getContext()); + } + } + private Intent getIntentWithOptions(Options options, Uri outputUri) { - UCrop uCrop = UCrop.of(uri, outputUri); + Uri uri = Uri.fromFile(fileData.getFile()); - uCrop = uCrop.withOptions(options); - if (options.getX() != 0) uCrop = uCrop.withAspectRatio(options.getX(), options.getY()); + UCrop uCrop = UCrop.of(uri, outputUri).withOptions(options); + if (options.getX() != 0) { + uCrop.withAspectRatio(options.getX(), options.getY()); + } if (options.getWidth() != 0) { - uCrop = uCrop.withMaxResultSize(options.getWidth(), options.getHeight()); + uCrop.withMaxResultSize(options.getWidth(), options.getHeight()); } return uCrop.getIntent(targetUi.getContext()); } - private Observable getInputUri() { - return getPath.with(uri).react().map(new Function() { - @Override public Uri apply(String filePath) throws Exception { - return Uri.fromFile(new File(filePath)).buildUpon().build(); - } - }); + private Uri getInputUri() { + return Uri.fromFile(fileData.getFile()); } - private Observable getOutputUriCrop() { - return getPath.with(uri).react().flatMap(new Function>() { - @Override public ObservableSource apply(String filepath) throws Exception { - String extension = imageUtils.getFileExtension(filepath); - String filename = Constants.CROP_APPEND + extension; - File file = imageUtils.getPrivateFile(filename); - return Observable.just(Uri.fromFile(file).buildUpon().build()); - } - }); - } + private File getOutputFile() { + String destination = fileData.getFile().getAbsolutePath(); + String extension = imageUtils.getFileExtension(destination, ImageUtils.JPG_FILE_EXTENSION); + String directory = config.getFileProviderDirectory(); + String outputFilename = imageUtils.createTimestampedFilename(CROP_APPEND, extension); + File outputFile = imageUtils.getPrivateFile(directory, outputFilename); - private Observable getOutputUriNoCrop() { - return getPath.with(uri).react().flatMap(new Function>() { - @Override public ObservableSource apply(String filepath) throws Exception { - String extension = imageUtils.getFileExtension(filepath); - String filename = Constants.NO_CROP_APPEND + extension; - File file = imageUtils.getPrivateFile(filename); - imageUtils.copy(new File(filepath), file); - return Observable.just(Uri.fromFile(file).buildUpon().build()); - } - }); + return outputFile; } + } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/Dimensions.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/Dimensions.java new file mode 100644 index 0000000..d483f73 --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/Dimensions.java @@ -0,0 +1,39 @@ +package com.miguelbcr.ui.rx_paparazzo2.interactors; + +import java.io.Serializable; + +public final class Dimensions implements Serializable { + + private static final long serialVersionUID = 1L; + + private int width; + private int height; + + public Dimensions() { + } + + public Dimensions(int width, int height) { + this.width = width; + this.height = height; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public boolean hasSize() { + return width > 0 && height > 0; + } +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/DownloadFile.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/DownloadFile.java new file mode 100644 index 0000000..ba86c29 --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/DownloadFile.java @@ -0,0 +1,146 @@ +/* + * Copyright 2016 Miguel Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.miguelbcr.ui.rx_paparazzo2.interactors; + +import android.net.Uri; +import android.os.Build; +import android.support.v4.provider.DocumentFile; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.schedulers.Schedulers; + +public final class DownloadFile extends UseCase { + + private static final String DOWNLOADED_FILENAME_PREFIX = "DOWNLOAD-"; + private static final String MATCH_ANYTHING_NOT_A_LETTER_OR_NUMBER = "[^A-Za-z0-9 ]"; + + private final TargetUi targetUi; + private final Config config; + private final ImageUtils imageUtils; + private Uri uri; + private FileData fileData; + + public DownloadFile(TargetUi targetUi, Config config, ImageUtils imageUtils) { + this.targetUi = targetUi; + this.config = config; + this.imageUtils = imageUtils; + } + + @Override + Observable react() { + return getObservableDownloadFile(); + } + + public DownloadFile with(Uri uri, FileData fileData) { + this.uri = uri; + this.fileData = fileData; + + return this; + } + + private Observable getObservableDownloadFile() { + return Observable.create(new ObservableOnSubscribe() { + @Override public void subscribe(ObservableEmitter subscriber) throws Exception { + if (!subscriber.isDisposed()) { + try { + if ("content".equalsIgnoreCase(uri.getScheme())) { + subscriber.onNext(getUsingContentResolver()); + } else { + subscriber.onNext(downloadFile()); + } + + subscriber.onComplete(); + } catch (FileNotFoundException e) { + subscriber.onError(e); + } + } + } + }).subscribeOn(Schedulers.io()); + } + + private FileData downloadFile() throws Exception { + String mimeType = imageUtils.getMimeType(targetUi.getContext(), uri); + String filename = getFilename(uri); + String fileExtension = imageUtils.getFileExtension(uri); + File file = imageUtils.getOutputFile(DOWNLOADED_FILENAME_PREFIX, fileExtension); + + URL url = new URL(uri.toString()); + URLConnection connection = url.openConnection(); + connection.connect(); + InputStream inputStream = new BufferedInputStream(url.openStream()); + imageUtils.copy(inputStream, file); + + return toFileData(mimeType, filename, file); + } + + private FileData toFileData(String mimeType, String filename, File destination) { + if (fileData == null || fileData.getFilename() == null) { + return new FileData(destination, true, filename, mimeType); + } else { + // maintain existing filename and mime-type unless missing + String fileMimeType = fileData.getMimeType(); + if (fileMimeType == null) { + fileMimeType = mimeType; + } + + return FileData.toFileDataDeleteSourceFileIfTransient(fileData, destination, true, fileMimeType); + } + } + + private FileData getUsingContentResolver() throws FileNotFoundException { + String mimeType = imageUtils.getMimeType(targetUi.getContext(), uri); + String uriFilename = getFilename(uri); + String fileExtension = imageUtils.getFileExtension(uri); + File file = imageUtils.getOutputFile(DOWNLOADED_FILENAME_PREFIX, fileExtension); + + InputStream inputStream = targetUi.getContext().getContentResolver().openInputStream(uri); + imageUtils.copy(inputStream, file); + + return toFileData(mimeType, uriFilename, file); + } + + private String getFilename(Uri uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + DocumentFile file = DocumentFile.fromSingleUri(targetUi.getContext(), uri); + if (file != null) { + String fileName = file.getName(); + if (fileName != null) { + return ImageUtils.stripPathFromFilename(fileName); + } + } + } + + String fileName = uri.getLastPathSegment(); + String safeFilename = fileName.replaceAll(MATCH_ANYTHING_NOT_A_LETTER_OR_NUMBER, ""); + + return safeFilename + "." + imageUtils.getFileExtension(uri); + } + +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/DownloadImage.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/DownloadImage.java deleted file mode 100644 index 47b41c1..0000000 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/DownloadImage.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2016 Miguel Garcia - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.miguelbcr.ui.rx_paparazzo2.interactors; - -import android.net.Uri; -import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; -import io.reactivex.Observable; -import io.reactivex.ObservableEmitter; -import io.reactivex.ObservableOnSubscribe; -import io.reactivex.schedulers.Schedulers; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; - -public final class DownloadImage extends UseCase { - private final TargetUi targetUi; - private final ImageUtils imageUtils; - private Uri uri; - - public DownloadImage(TargetUi targetUi, ImageUtils imageUtils) { - this.targetUi = targetUi; - this.imageUtils = imageUtils; - } - - @Override Observable react() { - return getObservableDownloadFile(); - } - - public DownloadImage with(Uri uri) { - this.uri = uri; - return this; - } - - private Observable getObservableDownloadFile() { - return Observable.create(new ObservableOnSubscribe() { - @Override public void subscribe(ObservableEmitter subscriber) throws Exception { - if (!subscriber.isDisposed()) { - try { - if ("content".equalsIgnoreCase(uri.getScheme())) { - subscriber.onNext(getContent()); - } else { - subscriber.onNext(downloadFile()); - } - - subscriber.onComplete(); - } catch (FileNotFoundException e) { - subscriber.onError(e); - } - } - } - }).subscribeOn(Schedulers.io()); - } - - private String downloadFile() throws Exception { - URL url = new URL(uri.toString()); - URLConnection connection = url.openConnection(); - connection.connect(); - InputStream inputStream = new BufferedInputStream(url.openStream(), 1024); - String filename = getFilename(uri); - filename += imageUtils.getFileExtension(uri); - File file = imageUtils.getPrivateFile(filename); - imageUtils.copy(inputStream, file); - return file.getAbsolutePath(); - } - - private String getContent() throws FileNotFoundException { - InputStream inputStream = targetUi.getContext().getContentResolver().openInputStream(uri); - String filename = getFilename(uri); - filename += imageUtils.getFileExtension(uri); - File file = imageUtils.getPrivateFile(filename); - imageUtils.copy(inputStream, file); - return file.getAbsolutePath(); - } - - private String getFilename(Uri uri) { - // Remove non alphanumeric characters - return uri.getLastPathSegment().replaceAll("[^A-Za-z0-9 ]", ""); - } -} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GetDimens.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GetDimens.java deleted file mode 100644 index 12f84d7..0000000 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GetDimens.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2016 Miguel Garcia - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.miguelbcr.ui.rx_paparazzo2.interactors; - -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.util.DisplayMetrics; -import com.miguelbcr.ui.rx_paparazzo2.entities.Config; -import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; -import com.miguelbcr.ui.rx_paparazzo2.entities.size.CustomMaxSize; -import com.miguelbcr.ui.rx_paparazzo2.entities.size.OriginalSize; -import com.miguelbcr.ui.rx_paparazzo2.entities.size.ScreenSize; -import io.reactivex.Observable; -import io.reactivex.functions.Function; - -public final class GetDimens extends UseCase { - private final TargetUi targetUi; - private final Config config; - private final GetPath getPath; - private Uri uri; - - public GetDimens(TargetUi targetUi, Config config, GetPath getPath) { - this.targetUi = targetUi; - this.config = config; - this.getPath = getPath; - } - - public GetDimens with(Uri uri) { - this.uri = uri; - return this; - } - - @Override public Observable react() { - return getPath.with(uri).react().map(new Function() { - @Override public int[] apply(String filePath) throws Exception { - if (config.getSize() instanceof OriginalSize) { - return GetDimens.this.getFileDimens(filePath); - } else if (config.getSize() instanceof CustomMaxSize) { - CustomMaxSize customMaxSize = (CustomMaxSize) config.getSize(); - return getCustomDimens(customMaxSize, filePath); - } else if (config.getSize() instanceof ScreenSize) { - return GetDimens.this.getScreenDimens(); - } else { - int[] dimens = GetDimens.this.getScreenDimens(); - return new int[] { dimens[0] / 8, dimens[1] / 8 }; - } - } - }); - } - - private int[] getScreenDimens() { - DisplayMetrics metrics = targetUi.getContext().getResources().getDisplayMetrics(); - return new int[] { metrics.widthPixels, metrics.heightPixels }; - } - - private int[] getFileDimens(String filePath) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(filePath, options); - return new int[] { options.outWidth, options.outHeight }; - } - - private int[] getCustomDimens(CustomMaxSize customMaxSize, String filePath) { - int maxSize = customMaxSize.getMaxImageSize(); - int[] dimens = GetDimens.this.getFileDimens(filePath); - int maxFileSize = Math.max(dimens[0], dimens[1]); - if (maxFileSize < maxSize) { - return dimens; - } - float scaleFactor = (float) maxSize / maxFileSize; - dimens[0] *= scaleFactor; - dimens[1] *= scaleFactor; - return dimens; - } -} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GetPath.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GetPath.java index fd60b0e..3c3865c 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GetPath.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GetPath.java @@ -25,17 +25,43 @@ import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.support.annotation.Nullable; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; + +import java.io.File; + import io.reactivex.Observable; -public final class GetPath extends UseCase { +public final class GetPath extends UseCase { + + private static final String URI_SCHEME_CONTENT = "content"; + private static final String URI_SCHEME_FILE = "file"; + private static final String PUBLIC_DOWNLOADS_URI = "content://downloads/public_downloads"; + private static final String DOCUMENT_AUTHORITY_EXTERNAL_STORAGE = "com.android.externalstorage.documents"; + private static final String DOCUMENT_AUTHORITY_DOWNLOADS = "com.android.providers.downloads.documents"; + private static final String DOCUMENT_AUTHORITY_MEDIA = "com.android.providers.media.documents"; + private static final String DOCUMENT_TYPE_PRIMARY = "primary"; + private static final String DOCUMENT_TYPE_IMAGE = "image"; + private static final String DOCUMENT_TYPE_VIDEO = "video"; + private static final String DOCUMENT_TYPE_AUDIO = "audio"; + + private static class Document { + String type; + String id; + } + + private final Config config; private final TargetUi targetUi; - private final DownloadImage downloadImage; + private final DownloadFile downloadFile; private Uri uri; - public GetPath(TargetUi targetUi, DownloadImage downloadImage) { + public GetPath(Config config, TargetUi targetUi, DownloadFile downloadFile) { + this.config = config; this.targetUi = targetUi; - this.downloadImage = downloadImage; + this.downloadFile = downloadFile; } public GetPath with(Uri uri) { @@ -43,83 +69,141 @@ public GetPath with(Uri uri) { return this; } - @Override public Observable react() { - return getPath(); + @Override + public Observable react() { + return getFileData(); } - @SuppressLint("NewApi") private Observable getPath() { - boolean isFromKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - Context context = targetUi.activity(); - String filePath = null; + @SuppressLint("NewApi") + private Observable getFileData() { + Context context = targetUi.getContext(); - if (uri == null) { + if (uri == null || context == null) { return null; } - if (isFromKitKat && DocumentsContract.isDocumentUri(context, uri)) { + FileData fileData = getFileData(context); + + if (fileData != null && fileData.getFile() != null) { + return Observable.just(fileData); + } + + return downloadFile.with(uri, fileData).react(); + } + + @Nullable + private FileData getFileData(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) { if (isExternalStorageDocument(uri)) { Document document = getDocument(uri); - if ("primary".equalsIgnoreCase(document.type)) { - filePath = Environment.getExternalStorageDirectory() + "/" + document.id; + + if (DOCUMENT_TYPE_PRIMARY.equalsIgnoreCase(document.type)) { + return getPrimaryExternalDocument(document); } } else if (isDownloadsDocument(uri)) { - String id = DocumentsContract.getDocumentId(uri); - Uri contentUri = - ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), - Long.valueOf(id)); - filePath = getDataColumn(context, contentUri, null, null); + return getDownloadsDocument(context); } else if (isMediaDocument(uri)) { - Document document = getDocument(uri); - Uri contentUri = null; - if ("image".equals(document.type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(document.type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(document.type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - } - - filePath = getDataColumn(context, contentUri, MediaStore.Images.Media._ID + "=?", - new String[] { document.id }); + return getMediaDocument(context); } - } else if ("content".equalsIgnoreCase(uri.getScheme())) { - filePath = getDataColumn(context, uri, null, null); - } else if ("file".equalsIgnoreCase(uri.getScheme())) { - filePath = uri.getPath(); + } else if (URI_SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) { + if (!isFileProvider(context)) { + return getDataColumn(context, uri, null, null); + } + } else if (URI_SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) { + return getFile(context); } - if (filePath == null) { - return downloadImage.with(uri).react(); + return null; + } + + private FileData getFile(Context context) { + File file = new File(uri.getPath()); + String fileName = ImageUtils.getFileName(uri.getPath()); + String mimeType = ImageUtils.getMimeType(context, uri); + + return new FileData(file, false, fileName, mimeType); + } + + private boolean isFileProvider(Context context) { + String authority = config.getFileProviderAuthority(context); + + return uri.getPath().startsWith(authority); + } + + private FileData getPrimaryExternalDocument(Document document) { + String mimeType = ImageUtils.getMimeType(document.id); + String fileName = ImageUtils.stripPathFromFilename(document.id); + String filePath = Environment.getExternalStorageDirectory() + "/" + document.id; + File file = new File(filePath); + + return new FileData(file, false, fileName, mimeType); + } + + private FileData getDownloadsDocument(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + String id = DocumentsContract.getDocumentId(uri); + Uri contentUri = ContentUris.withAppendedId(Uri.parse(PUBLIC_DOWNLOADS_URI), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); } - return Observable.just(filePath); + throw new IllegalStateException("Minimum Android API version must be be KitKat to use DocumentsContract API"); } - private class Document { - String type; - String id; + private FileData getMediaDocument(Context context) { + Document document = getDocument(uri); + Uri contentUri = null; + if (DOCUMENT_TYPE_IMAGE.equals(document.type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if (DOCUMENT_TYPE_VIDEO.equals(document.type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if (DOCUMENT_TYPE_AUDIO.equals(document.type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + return getDataColumn(context, contentUri, MediaStore.Images.Media._ID + "=?", new String[] { document.id }); } - @SuppressLint("NewApi") private Document getDocument(Uri uri) { + @SuppressLint("NewApi") + private Document getDocument(Uri uri) { Document document = new Document(); String docId = DocumentsContract.getDocumentId(uri); String[] docArray = docId.split(":"); document.type = docArray[0]; document.id = docArray[1]; + return document; } - private String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + private FileData getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; - String column = MediaStore.Images.Media.DATA; - String[] projection = { column }; + String dataColumn = MediaStore.Images.Media.DATA; + String nameColumn = MediaStore.Images.Media.DISPLAY_NAME; + String mimeTypeColumn = MediaStore.Images.Media.MIME_TYPE; + String titleColumn = MediaStore.Images.Media.TITLE; + + String[] projection = { dataColumn, nameColumn, mimeTypeColumn, titleColumn }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); - cursor.moveToFirst(); - return cursor.getString(cursor.getColumnIndexOrThrow(column)); + if (cursor != null && cursor.moveToFirst()) { + String filePath = cursor.getString(cursor.getColumnIndexOrThrow(dataColumn)); + String fileName = cursor.getString(cursor.getColumnIndexOrThrow(nameColumn)); + String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(mimeTypeColumn)); + String title = cursor.getString(cursor.getColumnIndexOrThrow(titleColumn)); + + File file; + if (filePath != null) { + file = new File(filePath); + } else { + file = null; + } + + return new FileData(file, false, fileName, mimeType, title); + } else { + return null; + } } catch (Exception e) { - // throw Exceptions.propagate(e); return null; } finally { if (cursor != null) { @@ -129,14 +213,14 @@ private String getDataColumn(Context context, Uri uri, String selection, String[ } private boolean isExternalStorageDocument(Uri uri) { - return "com.android.externalstorage.documents".equals(uri.getAuthority()); + return DOCUMENT_AUTHORITY_EXTERNAL_STORAGE.equals(uri.getAuthority()); } private boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + return DOCUMENT_AUTHORITY_DOWNLOADS.equals(uri.getAuthority()); } private boolean isMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); + return DOCUMENT_AUTHORITY_MEDIA.equals(uri.getAuthority()); } } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GrantPermissions.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GrantPermissions.java index d19c03e..84d42f1 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GrantPermissions.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/GrantPermissions.java @@ -57,7 +57,7 @@ public GrantPermissions with(String... permissions) { return permissions; } }) - .flatMap(new Function>() { + .concatMap(new Function>() { @Override public ObservableSource apply(Permission permission) throws Exception { if (permission.granted) { return Observable.just(Activity.RESULT_OK); diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/ImageUtils.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/ImageUtils.java index ab9e4da..bad83ed 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/ImageUtils.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/ImageUtils.java @@ -24,11 +24,14 @@ import android.net.Uri; import android.os.Environment; import android.text.TextUtils; +import android.util.Log; import android.webkit.MimeTypeMap; + import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; import com.miguelbcr.ui.rx_paparazzo2.entities.size.OriginalSize; -import io.reactivex.exceptions.Exceptions; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -39,7 +42,22 @@ import java.util.Date; import java.util.Locale; +import io.reactivex.exceptions.Exceptions; + public final class ImageUtils { + + private static final String TAG = ImageUtils.class.getSimpleName(); + private static final String DEFAULT_EXTENSION = ""; + public static final String JPG_FILE_EXTENSION = "jpg"; + private static final String PNG_FILE_EXTENSION = "png"; + + public static final String MIME_TYPE_IMAGE_WILDCARD = "image/*"; + public static final String MIME_TYPE_JPEG = "image/jpeg"; + private static final String MIME_TYPE_PNG = "image/png"; + + private static final String DATE_FORMAT = "yyyyMMdd_HHmm_ssSSS"; + private static final String LOCALE_EN = "en"; + private final TargetUi targetUi; private final Config config; @@ -48,41 +66,52 @@ public ImageUtils(TargetUi targetUi, Config config) { this.config = config; } - File getOutputFile(String extension) { - String dirname = getApplicationName(targetUi.getContext()); - File dir = getDir(null, dirname); - return getFile(dir, extension); + public File getOutputFile(String prefix, String extension) { + String fileProviderDirectory = config.getFileProviderDirectory(); + File dir = getDir(null, fileProviderDirectory); + + return createTimestampedFile(dir, prefix, extension); } - private File getFile(File dir, String extension) { - SimpleDateFormat simpleDateFormat = - new SimpleDateFormat(Constants.DATE_FORMAT, new Locale(Constants.LOCALE_EN)); - String datetime = simpleDateFormat.format(new Date()); - File file = new File(dir.getAbsolutePath(), "IMG-" + datetime + extension); + private File createTimestampedFile(File dir, String prefix, String extension) { + File file = new File(dir.getAbsolutePath(), createTimestampedFilename(prefix, extension)); while (file.exists()) { - datetime = simpleDateFormat.format(new Date()); - file = new File(dir.getAbsolutePath(), "IMG-" + datetime + extension); + String filename = createTimestampedFilename(prefix, extension); + file = new File(dir.getAbsolutePath(), filename); } return file; } - File getPrivateFile(String filename) { - File dir = new File(targetUi.getContext().getFilesDir(), Constants.SUBDIR); + public String createTimestampedFilename(String prefix, String extension) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_FORMAT, new Locale(LOCALE_EN)); + String datetime = simpleDateFormat.format(new Date()); + + if (!TextUtils.isEmpty(extension) && !extension.startsWith(".")) { + extension = "." + extension; + } + + return prefix + datetime + extension; + } + + public File getPrivateFile(String directory, String filename) { + File dir = new File(targetUi.getContext().getFilesDir(), directory); dir.mkdirs(); + return new File(dir, filename); } private String getApplicationName(Context context) { int stringId = context.getApplicationInfo().labelRes; + return context.getString(stringId); } private File getDir(String dirRoot, String dirname) { File storageDir = null; - if (!config.useInternalStorage()) { + if (!config.isUseInternalStorage()) { storageDir = getPublicDir(dirRoot, dirname); } @@ -97,10 +126,14 @@ private File getPublicDir(String dirRoot, String dirname) { File storageDir = null; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - File dir = (dirRoot != null) ? Environment.getExternalStoragePublicDirectory(dirRoot) - : Environment.getExternalStorageDirectory(); - storageDir = new File(dir, dirname); + File dir; + if (dirRoot == null) { + dir = Environment.getExternalStorageDirectory(); + } else { + dir = Environment.getExternalStoragePublicDirectory(dirRoot); + } + storageDir = new File(dir, dirname); if (!storageDir.exists() && !storageDir.mkdirs()) { storageDir = null; } @@ -121,70 +154,132 @@ private File getPrivateDir(String dirname) { return storageDir; } - String getFileExtension(String filepath) { + public static String getFileName(String filepath) { + File file = new File(filepath); + + return file.getName(); + } + + public static String stripPathFromFilename(String fileName) { + int lastSlash = fileName.lastIndexOf("/"); + if (lastSlash == -1) { + return fileName; + } else { + return fileName.substring(lastSlash + 1); + } + } + + public String getFileExtension(String filepath) { + return getFileExtension(filepath, DEFAULT_EXTENSION); + } + + public String getFileExtension(String filepath, String defaultExtension) { String extension = ""; if (filepath != null) { int i = filepath.lastIndexOf('.'); - extension = i > 0 ? filepath.substring(i) : ""; + if (i > 0) { + extension = filepath.substring(i + 1); + } else { + extension = ""; + } } - return (TextUtils.isEmpty(extension)) ? ".jpg" : extension; + if (TextUtils.isEmpty(extension) && !TextUtils.isEmpty(defaultExtension)) { + extension = defaultExtension; + } + + return extension; } String getFileExtension(Uri uri) { + String mimeType = getMimeType(targetUi.getContext(), uri); + + if (TextUtils.isEmpty(mimeType)) { + return getFileExtension(uri.getLastPathSegment()); + } else { + return mimeType.split("/")[1]; + } + } + + public static String getMimeType(Context context, Uri uri) { String mimeType; if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - mimeType = targetUi.getContext().getContentResolver().getType(uri); + mimeType = context.getContentResolver().getType(uri); } else { - String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); - mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()); + String path = uri.toString(); + mimeType = getMimeType(path); } + return mimeType; + } - return (TextUtils.isEmpty(mimeType)) ? getFileExtension(uri.getLastPathSegment()) - : "." + mimeType.split("/")[1]; + public static String getMimeType(String path) { + String fileExtension = MimeTypeMap.getFileExtensionFromUrl(path); + + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()); } - String scaleImage(String filePath, String filePathOutput, int[] dimens) { + public static Dimensions getImageDimensions(File file) { + String filePath = file.getAbsolutePath(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filePath, options); + + return new Dimensions(options.outWidth, options.outHeight); + } + + FileData scaleImage(FileData inputData, File destination, Dimensions dimens) { + File input = inputData.getFile(); + String mimeType; + if (config.getSize() instanceof OriginalSize) { - copyFileAndExifTags(filePath, filePathOutput, dimens); - return filePathOutput; - } + copyFileAndExifTags(input, destination, dimens); + mimeType = inputData.getMimeType(); + } else { + Bitmap bitmap = sampleBitmap(input, dimens.getWidth(), dimens.getHeight()); + if (bitmap == null) { + copyFileAndExifTags(input, destination, dimens); + mimeType = inputData.getMimeType(); + } else { + Bitmap.CompressFormat compressFormat = getCompressFormat(destination.getName()); + if (Bitmap.CompressFormat.JPEG == compressFormat) { + mimeType = MIME_TYPE_JPEG; + } else if (Bitmap.CompressFormat.PNG == compressFormat) { + mimeType = MIME_TYPE_PNG; + } else { + throw new IllegalStateException(String.format("Received unexpected compression format '%s'", compressFormat)); + } - Bitmap bitmap = handleBitmapSampling(filePath, dimens[0], dimens[1]); - if (bitmap == null) { - copyFileAndExifTags(filePath, filePathOutput, dimens); - return filePathOutput; + bitmap2file(bitmap, destination, compressFormat); + copyExifTags(input, destination, dimens); + } } - bitmap2file(bitmap, new File(filePathOutput), getCompressFormat(filePathOutput)); - copyExifTags(filePath, filePathOutput, dimens); - - return filePathOutput; + return new FileData(inputData, destination, true, mimeType); } private Bitmap.CompressFormat getCompressFormat(String filePath) { Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.JPEG; - final String extension = getFileExtension(filePath); + String extension = getFileExtension(filePath); - if (extension.toLowerCase().contains(Constants.EXT_PNG)) { + if (extension.toLowerCase().contains(PNG_FILE_EXTENSION)) { compressFormat = Bitmap.CompressFormat.PNG; } return compressFormat; } - private void copyFileAndExifTags(String filePath, String filePathOutput, int[] dimens) { - copy(new File(filePath), new File(filePathOutput)); - copyExifTags(filePath, filePathOutput, dimens); + private void copyFileAndExifTags(File input, File fileOutput, Dimensions dimens) { + copy(input, fileOutput); + copyExifTags(input, fileOutput, dimens); } - public void copy(InputStream in, File dst) { + public void copy(InputStream in, File destination) { OutputStream out = null; try { - out = new FileOutputStream(dst); + out = new FileOutputStream(destination); byte[] buffer = new byte[1024]; int length; @@ -192,7 +287,9 @@ public void copy(InputStream in, File dst) { out.write(buffer, 0, length); } } catch (IOException e) { - e.printStackTrace(); + String message = String.format("Could not copy file to '%s'", destination.getAbsolutePath()); + Log.e(TAG, message, e); + throw Exceptions.propagate(e); } finally { try { @@ -209,23 +306,27 @@ public void copy(InputStream in, File dst) { } } - public void copy(File src, File dst) { + public void copy(File source, File destination) { try { - InputStream in = new FileInputStream(src); - copy(in, dst); + InputStream in = new FileInputStream(source); + copy(in, destination); } catch (IOException e) { - e.printStackTrace(); + String message = String.format("Could not copy file to '%s'", destination.getAbsolutePath()); + Log.e(TAG, message, e); + throw Exceptions.propagate(e); } } - private void bitmap2file(Bitmap bitmap, File file, Bitmap.CompressFormat compressFormat) { + private void bitmap2file(Bitmap bitmap, File destination, Bitmap.CompressFormat compressFormat) { FileOutputStream fileOutputStream = null; try { - fileOutputStream = new FileOutputStream(file); + fileOutputStream = new FileOutputStream(destination); bitmap.compress(compressFormat, 90, fileOutputStream); } catch (Exception e) { - e.printStackTrace(); + String message = String.format("Could not save bitmap file to '%s'", destination.getAbsolutePath()); + Log.e(TAG, message, e); + throw Exceptions.propagate(e); } finally { try { @@ -239,26 +340,29 @@ private void bitmap2file(Bitmap bitmap, File file, Bitmap.CompressFormat compres } } - private void copyExifTags(String srcFilePath, String dstFilePath, int[] dimens) { - if (getCompressFormat(dstFilePath) == Bitmap.CompressFormat.JPEG) { - try { - ExifInterface exifSource = new ExifInterface(srcFilePath); - ExifInterface exifDest = new ExifInterface(dstFilePath); - - for (String attribute : getExifTags()) { - String tagValue = exifSource.getAttribute(attribute); - - if (!TextUtils.isEmpty(tagValue)) { - exifDest.setAttribute(attribute, tagValue); + private void copyExifTags(File source, File destination, Dimensions dimens) { + try { + String destinationPath = destination.getAbsolutePath(); + String sourcePath = source.getAbsolutePath(); + if (getCompressFormat(destinationPath) == Bitmap.CompressFormat.JPEG) { + ExifInterface exifSource = new ExifInterface(sourcePath); + ExifInterface exifDest = new ExifInterface(destinationPath); + + for (String attribute : getExifTags()) { + String tagValue = exifSource.getAttribute(attribute); + + if (!TextUtils.isEmpty(tagValue)) { + exifDest.setAttribute(attribute, tagValue); + } } - } - exifDest.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(dimens[0])); - exifDest.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(dimens[1])); - exifDest.saveAttributes(); - } catch (IOException e) { - e.printStackTrace(); + exifDest.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(dimens.getWidth())); + exifDest.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(dimens.getHeight())); + exifDest.saveAttributes(); } + } catch (IOException e) { + String message = String.format("Could not copy exif tags from '%s'", source.getAbsolutePath()); + Log.d(TAG, message, e); } } @@ -278,29 +382,50 @@ private String[] getExifTags() { }; } - private Bitmap handleBitmapSampling(String filePath, int maxWidth, int maxHeight) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(filePath, options); - options.inSampleSize = - (maxWidth <= 0 || maxHeight <= 0) ? 1 : calculateInSampleSize(options, maxWidth, maxHeight); - + private Bitmap sampleBitmap(File input, int maxWidth, int maxHeight) { + BitmapFactory.Options options = sampleSize(input, maxWidth, maxHeight); if (options.inSampleSize == 1) { return null; } options.inJustDecodeBounds = false; + + String filePath = input.getAbsolutePath(); + return BitmapFactory.decodeFile(filePath, options); } + public boolean isImage(File input) { + BitmapFactory.Options options = sampleSize(input, 0, 0); + + return options.outWidth > 0 && options.outHeight > 0; + } + + private BitmapFactory.Options sampleSize(File input, int maxWidth, int maxHeight) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + + // load dimensions + String filePath = input.getAbsolutePath(); + BitmapFactory.decodeFile(filePath, options); + + if (maxWidth <= 0 || maxHeight <= 0) { + options.inSampleSize = 1; + } else { + options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); + } + + return options; + } + private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) { int inSampleSize = 1; - int[] dimensPortrait = getDimensionsPortrait(options.outWidth, options.outHeight); - int[] maxDimensPortrait = getDimensionsPortrait(maxWidth, maxHeight); - float width = dimensPortrait[0]; - float height = dimensPortrait[1]; - float newMaxWidth = maxDimensPortrait[0]; - float newMaxHeight = maxDimensPortrait[1]; + Dimensions dimensPortrait = getDimensionsPortrait(options.outWidth, options.outHeight); + Dimensions maxDimensPortrait = getDimensionsPortrait(maxWidth, maxHeight); + float width = dimensPortrait.getWidth(); + float height = dimensPortrait.getHeight(); + float newMaxWidth = maxDimensPortrait.getWidth(); + float newMaxHeight = maxDimensPortrait.getHeight(); if (height > newMaxHeight || width > newMaxWidth) { int heightRatio = Math.round(height / newMaxHeight); @@ -317,11 +442,11 @@ private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, i return inSampleSize; } - private int[] getDimensionsPortrait(int width, int height) { + private Dimensions getDimensionsPortrait(int width, int height) { if (width < height) { - return new int[] { width, height }; + return new Dimensions(width, height); } else { - return new int[] { height, width }; + return new Dimensions(height, width); } } } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PermissionUtil.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PermissionUtil.java new file mode 100644 index 0000000..a4bd09a --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PermissionUtil.java @@ -0,0 +1,65 @@ +package com.miguelbcr.ui.rx_paparazzo2.interactors; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; + +import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; + +import java.util.List; + +public class PermissionUtil { + + public static final int READ_WRITE_PERMISSIONS = Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + + public static Intent requestReadWritePermission(TargetUi targetUi, Intent intent, Uri uri) { + intent.addFlags(READ_WRITE_PERMISSIONS); + + grantFileReadWritePermissions(targetUi, intent, uri); + + return intent; + } + + public static void grantReadPermissionToUri(TargetUi targetUi, Uri uri) { + String uiPackageName = targetUi.getContext().getPackageName(); + + targetUi.getContext().grantUriPermission(uiPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + /** + * Workaround for Android bug.
+ * See https://code.google.com/p/android/issues/detail?id=76683
+ * See http://stackoverflow.com/questions/18249007/how-to-use-support-fileprovider-for-sharing-content-to-other-apps + */ + private static void grantFileReadWritePermissions(TargetUi targetUi, Intent intent, Uri uri) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + List resInfoList = targetUi.getContext() + .getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + targetUi.getContext().grantUriPermission(packageName, uri, READ_WRITE_PERMISSIONS); + } + } + } + + public static void revokeFileReadWritePermissions(TargetUi targetUi, Uri uri) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + targetUi.getContext().revokeUriPermission(uri, READ_WRITE_PERMISSIONS); + } + } + + public static String[] getReadAndWriteStoragePermissions(boolean internal) { + if (internal) { + return new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }; + } else { + return new String[] { + Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE + }; + } + } +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickFile.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickFile.java new file mode 100644 index 0000000..6bf9111 --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickFile.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 Miguel Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.miguelbcr.ui.rx_paparazzo2.interactors; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.Nullable; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; + +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import rx_activity_result2.OnPreResult; + +public class PickFile extends UseCase { + + public static final String DEFAULT_MIME_TYPE = "*/*"; + + private final Config config; + private final StartIntent startIntent; + private final TargetUi targetUi; + + public PickFile(TargetUi targetUi, Config config, StartIntent startIntent) { + this.targetUi = targetUi; + this.config = config; + this.startIntent = startIntent; + } + + public String getDefaultMimeType() { + return DEFAULT_MIME_TYPE; + } + + @Override + public Observable react() { + return startIntent.with(getFileChooserIntent(), getOnPreResultProcessing()) + .react() + .map(new Function() { + @Override public Uri apply(Intent intent) throws Exception { + return intent.getData(); + } + }); + } + + private Intent getFileChooserIntent() { + String mimeType = config.getMimeType(getDefaultMimeType()); + Intent intent = new Intent(); + intent.setType(mimeType); + + if (config.isUseDocumentPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ + intent.setAction(Intent.ACTION_OPEN_DOCUMENT); + } else { + intent.setAction(Intent.ACTION_GET_CONTENT); + } + + if (config.isPickOpenableOnly()) { + intent.addCategory(Intent.CATEGORY_OPENABLE); + } + + return intent; + } + + private OnPreResult getOnPreResultProcessing() { + return new OnPreResult() { + @Override + public Observable response(int responseCode, @Nullable final Intent intent) { + if (responseCode == Activity.RESULT_OK && intent != null && intent.getData() != null) { + + Uri pickedUri = intent.getData(); + PermissionUtil.grantReadPermissionToUri(targetUi, pickedUri); + + return Observable.just(intent.getData()); + } else { + return Observable.empty(); + } + } + }; + } + +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickImages.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickFiles.java similarity index 58% rename from rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickImages.java rename to rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickFiles.java index 2edbe8f..8b4f4fc 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickImages.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickFiles.java @@ -20,26 +20,51 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; -import io.reactivex.Observable; -import io.reactivex.functions.Function; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public final class PickImages extends UseCase> { +import io.reactivex.Observable; +import io.reactivex.functions.Function; + +public class PickFiles extends UseCase> { + + public static final String DEFAULT_MIME_TYPE = "*/*"; + + private final TargetUi targetUi; + private final Config config; private final StartIntent startIntent; - public PickImages(StartIntent startIntent) { + public PickFiles(TargetUi targetUi, Config config, StartIntent startIntent) { + this.targetUi = targetUi; + this.config = config; this.startIntent = startIntent; } + public String getDefaultMimeType() { + return DEFAULT_MIME_TYPE; + } + @Override public Observable> react() { return startIntent.with(getFileChooserIntent()).react().map(new Function>() { @Override public List apply(Intent intent) throws Exception { - if (intent.getData() != null) { - return Arrays.asList(intent.getData()); + if (intent == null) { + return new ArrayList<>(); + } + + intent.addFlags(PermissionUtil.READ_WRITE_PERMISSIONS); + + Uri pickedUri = intent.getData(); + if (pickedUri != null) { + PermissionUtil.grantReadPermissionToUri(targetUi, pickedUri); + + return Arrays.asList(pickedUri); } else { - return PickImages.this.getUris(intent); + return getUris(intent); } } }); @@ -53,6 +78,9 @@ private List getUris(Intent intent) { for (int i = 0; i < clipData.getItemCount(); i++) { ClipData.Item item = clipData.getItemAt(i); Uri uri = item.getUri(); + + PermissionUtil.grantReadPermissionToUri(targetUi, uri); + uris.add(uri); } } @@ -61,9 +89,19 @@ private List getUris(Intent intent) { } private Intent getFileChooserIntent() { + String mimeType = config.getMimeType(getDefaultMimeType()); Intent intent = new Intent(); - intent.setType("image/*"); - intent.setAction(Intent.ACTION_GET_CONTENT); + intent.setType(mimeType); + + if (config.isUseDocumentPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ + intent.setAction(Intent.ACTION_OPEN_DOCUMENT); + } else { + intent.setAction(Intent.ACTION_GET_CONTENT); + } + + if (config.isPickOpenableOnly()) { + intent.addCategory(Intent.CATEGORY_OPENABLE); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickImage.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickImage.java deleted file mode 100644 index bc8c5ed..0000000 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/PickImage.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2016 Miguel Garcia - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.miguelbcr.ui.rx_paparazzo2.interactors; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.exceptions.Exceptions; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; -import java.io.File; -import rx_activity_result2.OnPreResult; - -public final class PickImage extends UseCase { - private final StartIntent startIntent; - private final GetPath getPath; - - public PickImage(StartIntent startIntent, GetPath getPath) { - this.startIntent = startIntent; - this.getPath = getPath; - } - - @Override public Observable react() { - return startIntent.with(getFileChooserIntent(), getOnPreResultProcessing()) - .react() - .map(new Function() { - @Override public Uri apply(Intent intent) throws Exception { - return intent.getData(); - } - }); - } - - private Intent getFileChooserIntent() { - Intent intent = new Intent(); - intent.setType("image/*"); - intent.setAction(Intent.ACTION_GET_CONTENT); - - return intent; - } - - private OnPreResult getOnPreResultProcessing() { - return new OnPreResult() { - @Override - public Observable response(int responseCode, @Nullable final Intent intent) { - if (responseCode == Activity.RESULT_OK) { - return getPath.with(intent.getData()) - .react() - .subscribeOn(Schedulers.io()) - .map(new Function() { - @Override public String apply(String filePath) throws Exception { - intent.setData(Uri.fromFile(new File(filePath))); - return filePath; - } - }) - .onErrorReturnItem(""); - } else { - return Observable.just(""); - } - } - }; - } -} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/SaveFile.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/SaveFile.java new file mode 100644 index 0000000..fb665b1 --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/SaveFile.java @@ -0,0 +1,157 @@ +/* + * Copyright 2016 Miguel Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.miguelbcr.ui.rx_paparazzo2.interactors; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.functions.Function; + +public final class SaveFile extends UseCase { + + private static final String TAG = SaveFile.class.getSimpleName(); + private static final String SAVED_FILE_PREFIX = "SAVED-"; + + private final TargetUi targetUi; + private final Config config; + private final ScaledImageDimensions scaledImageDimensions; + private final ImageUtils imageUtils; + + private FileData fileData; + + public SaveFile(TargetUi targetUi, Config config, ScaledImageDimensions scaledImageDimensions, ImageUtils imageUtils) { + this.targetUi = targetUi; + this.config = config; + this.scaledImageDimensions = scaledImageDimensions; + this.imageUtils = imageUtils; + } + + public SaveFile with(FileData fileData) { + this.fileData = fileData; + + return this; + } + + @Override + public Observable react() { + return scaledImageDimensions.with(fileData).react().flatMap(new Function>() { + @Override + public ObservableSource apply(Dimensions scaledDimensions) throws Exception { + return saveAndSendToMediaScanner(scaledDimensions); + } + }); + } + + private ObservableSource saveAndSendToMediaScanner(Dimensions scaledDimensions) throws Exception { + FileData saved = save(scaledDimensions); + + if (config.isSendToMediaScanner()) { + if (config.isUseInternalStorage()) { + File file = fileData.getFile(); + String message = String.format("Media scanner will not be able to access internal storage '%s'", file.getAbsolutePath()); + Log.w(TAG, message); + } + + if (saved.getFile() != null && saved.getFile().exists()) { +// sendToMediaScanner(saved); + sendToMediaScannerIntent(saved); + } + } + + return Observable.just(saved); + } + + private FileData save(Dimensions scaledDimensions) throws Exception { + Dimensions imageDimensions = ImageUtils.getImageDimensions(fileData.getFile()); + boolean isImage = imageDimensions.hasSize(); + if (isImage) { + FileData withDimensions = new FileData(fileData, imageDimensions); + + return saveImageAndDeleteSourceFile(withDimensions, scaledDimensions); + } else { + return saveToDestinationAndDeleteSourceFile(fileData); + } + } + + private FileData saveToDestinationAndDeleteSourceFile(FileData fileData) throws Exception { + File source = fileData.getFile(); + + if (isFileSizeLimitExceeded(source)) { + FileData.deleteSourceFile(fileData); + + return FileData.exceededMaximumFileSize(fileData); + } + + InputStream inputStream = new BufferedInputStream(new FileInputStream(source)); + File destination = getOutputFile(); + imageUtils.copy(inputStream, destination); + + return FileData.toFileDataDeleteSourceFileIfTransient(fileData, destination, true, fileData.getMimeType()); + } + + private FileData saveImageAndDeleteSourceFile(FileData fileData, Dimensions dimensions) { + FileData scaled = imageUtils.scaleImage(fileData, getOutputFile(), dimensions); + + if (isFileSizeLimitExceeded(scaled.getFile())) { + FileData.deleteSourceFile(fileData); + FileData.deleteSourceFile(scaled); + + return FileData.exceededMaximumFileSize(fileData); + } + + FileData.deleteSourceFile(fileData); + + return scaled; + } + + private boolean isFileSizeLimitExceeded(File scaledFile) { + return scaledFile.exists() && scaledFile.length() > config.getMaximumFileSize(); + } + + private void sendToMediaScannerIntent(FileData fileDataToScan) { + File file = fileDataToScan.getFile(); + if (file.exists()) { + Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + Context context = targetUi.getContext(); + Uri contentUri = Uri.fromFile(file); + mediaScanIntent.setData(contentUri); + + context.sendBroadcast(mediaScanIntent); + } + } + + private File getOutputFile() { + String fileName = fileData.getFilename(); + String extension = imageUtils.getFileExtension(fileName); + + return imageUtils.getOutputFile(SAVED_FILE_PREFIX, extension); + } + +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/SaveImage.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/SaveImage.java deleted file mode 100644 index 4a0f32d..0000000 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/SaveImage.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2016 Miguel Garcia - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.miguelbcr.ui.rx_paparazzo2.interactors; - -import android.media.MediaScannerConnection; -import android.net.Uri; -import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.functions.Function; -import io.reactivex.functions.Function3; -import java.io.File; - -public final class SaveImage extends UseCase { - private final TargetUi targetUi; - private final GetPath getPath; - private final GetDimens getDimens; - private final ImageUtils imageUtils; - private Uri uri; - - public SaveImage(TargetUi targetUi, GetPath getPath, GetDimens getDimens, ImageUtils imageUtils) { - this.targetUi = targetUi; - this.getPath = getPath; - this.getDimens = getDimens; - this.imageUtils = imageUtils; - } - - public SaveImage with(Uri uri) { - this.uri = uri; - return this; - } - - @Override public Observable react() { - return getOutputUri().flatMap(new Function>() { - @Override public ObservableSource apply(Uri outputUri) throws Exception { - return Observable.zip(getPath.with(uri).react(), getPath.with(outputUri).react(), - getDimens.with(uri).react(), new Function3() { - @Override public String apply(String filePath, String filePathOutput, int[] dimens) - throws Exception { - String filePathScaled = imageUtils.scaleImage(filePath, filePathOutput, dimens); - new File(filePath).delete(); - - MediaScannerConnection.scanFile(targetUi.getContext(), - new String[] { filePathScaled }, new String[] { "image/*" }, null); - - return filePathScaled; - } - }); - } - }); - } - - private Observable getOutputUri() { - return getPath.with(uri).react().flatMap(new Function>() { - @Override public ObservableSource apply(String filepath) throws Exception { - String extension = imageUtils.getFileExtension(filepath); - return Observable.just(Uri.fromFile(imageUtils.getOutputFile(extension))); - } - }); - } -} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/ScaledImageDimensions.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/ScaledImageDimensions.java new file mode 100644 index 0000000..8e0fd0d --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/ScaledImageDimensions.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016 Miguel Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.miguelbcr.ui.rx_paparazzo2.interactors; + +import android.util.DisplayMetrics; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.CustomMaxSize; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.OriginalSize; +import com.miguelbcr.ui.rx_paparazzo2.entities.size.ScreenSize; + +import java.io.File; + +import io.reactivex.Observable; + +public final class ScaledImageDimensions extends UseCase { + private final TargetUi targetUi; + private final Config config; + private FileData fileData; + + public ScaledImageDimensions(TargetUi targetUi, Config config) { + this.targetUi = targetUi; + this.config = config; + } + + public ScaledImageDimensions with(FileData fileData) { + this.fileData = fileData; + return this; + } + + @Override public Observable react() { + return Observable.just(getDimensions()); + } + + private Dimensions getDimensions() { + File file = fileData.getFile(); + if (config.getSize() instanceof OriginalSize) { + return ImageUtils.getImageDimensions(file); + } else if (config.getSize() instanceof CustomMaxSize) { + return getCustomDimens((CustomMaxSize) config.getSize(), file); + } else if (config.getSize() instanceof ScreenSize) { + return ScaledImageDimensions.this.getScreenDimens(); + } else { + Dimensions dimens = ScaledImageDimensions.this.getScreenDimens(); + + return new Dimensions(dimens.getWidth() / 8, dimens.getHeight() / 8); + } + } + + private Dimensions getScreenDimens() { + DisplayMetrics metrics = targetUi.getContext().getResources().getDisplayMetrics(); + + return new Dimensions(metrics.widthPixels, metrics.heightPixels); + } + + private Dimensions getCustomDimens(CustomMaxSize customMaxSize, File file) { + int maxSize = customMaxSize.getMaxImageSize(); + Dimensions dimensions = ImageUtils.getImageDimensions(file); + + int maxFileSize = Math.max(dimensions.getWidth(), dimensions.getHeight()); + if (maxFileSize < maxSize) { + return dimensions; + } + + float scaleFactor = (float) maxSize / maxFileSize; + float scaledWidth = dimensions.getWidth() * scaleFactor; + float scaledHeight = dimensions.getHeight() * scaleFactor; + + return new Dimensions((int)scaledWidth, (int)scaledHeight); + } +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/TakePhoto.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/TakePhoto.java index 2d74841..25e2c69 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/TakePhoto.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/interactors/TakePhoto.java @@ -18,76 +18,83 @@ import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.Build; import android.provider.MediaStore; import android.support.v4.content.FileProvider; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; + +import java.io.File; +import java.io.FileNotFoundException; + import io.reactivex.Observable; import io.reactivex.functions.Function; -import java.io.File; -import java.util.List; -public final class TakePhoto extends UseCase { - private static final int READ_WRITE_PERMISSIONS = - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; +public final class TakePhoto extends UseCase { + private static final String PHOTO_FILE_PREFIX = "PHOTO-"; + + private final Config config; private final StartIntent startIntent; private final TargetUi targetUi; private final ImageUtils imageUtils; - public TakePhoto(StartIntent startIntent, TargetUi targetUi, ImageUtils imageUtils) { + public TakePhoto(Config config, StartIntent startIntent, TargetUi targetUi, ImageUtils imageUtils) { + this.config = config; this.startIntent = startIntent; this.targetUi = targetUi; this.imageUtils = imageUtils; } - @Override public Observable react() { - final Uri uri = getUri(); - return startIntent.with(getIntentCamera(uri)).react().map(new Function() { + @Override + public Observable react() { + final File file = getOutputFile(); + Uri uri = getUri(file); + + return startIntent.with(getIntentCamera(uri)) + .react() + .map(revokeFileReadWritePermissions(targetUi, uri)) + .map(new Function() { + @Override + public FileData apply(Uri uri) throws Exception { + if (!file.exists()) { + throw new FileNotFoundException(String.format("Camera file not saved", file.getAbsolutePath())); + } + + return new FileData(file, true, file.getName(), ImageUtils.MIME_TYPE_JPEG); + } + }); + } + + private Function revokeFileReadWritePermissions(final TargetUi targetUi, final Uri uri) { + return new Function() { @Override public Uri apply(Intent data) throws Exception { - revokeFileReadWritePermissions(uri); + PermissionUtil.revokeFileReadWritePermissions(targetUi, uri); + return uri; } - }); + }; } - private Uri getUri() { + private Uri getUri(File file) { Context context = targetUi.getContext(); - File file = imageUtils.getPrivateFile(Constants.SHOOT_APPEND); - String authority = context.getPackageName() + "." + Constants.FILE_PROVIDER; + String authority = config.getFileProviderAuthority(context); + return FileProvider.getUriForFile(context, authority, file); } + private File getOutputFile() { + String filename = imageUtils.createTimestampedFilename(PHOTO_FILE_PREFIX, ImageUtils.JPG_FILE_EXTENSION); + String directory = config.getFileProviderDirectory(); + return imageUtils.getPrivateFile(directory, filename); + } + private Intent getIntentCamera(Uri uri) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - intent.addFlags(READ_WRITE_PERMISSIONS); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); - grantFileReadWritePermissions(intent, uri); - return intent; - } - /** - * Workaround for Android bug.
- * See https://code.google.com/p/android/issues/detail?id=76683
- * See http://stackoverflow.com/questions/18249007/how-to-use-support-fileprovider-for-sharing-content-to-other-apps - */ - private void grantFileReadWritePermissions(Intent intent, Uri uri) { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List resInfoList = targetUi.getContext() - .getPackageManager() - .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - targetUi.getContext().grantUriPermission(packageName, uri, READ_WRITE_PERMISSIONS); - } - } + return PermissionUtil.requestReadWritePermission(targetUi, intent, uri); } - private void revokeFileReadWritePermissions(Uri uri) { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - targetUi.getContext().revokeUriPermission(uri, READ_WRITE_PERMISSIONS); - } - } } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponent.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponent.java index 0d53579..aba9378 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponent.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponent.java @@ -18,13 +18,13 @@ import com.miguelbcr.ui.rx_paparazzo2.interactors.GetPath; import com.miguelbcr.ui.rx_paparazzo2.workers.Camera; -import com.miguelbcr.ui.rx_paparazzo2.workers.Gallery; +import com.miguelbcr.ui.rx_paparazzo2.workers.Files; public abstract class ApplicationComponent { public abstract Camera camera(); - public abstract Gallery gallery(); + public abstract Files files(); public abstract GetPath getPath(); diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponentImpl.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponentImpl.java index 0eaf201..4be376c 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponentImpl.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/internal/di/ApplicationComponentImpl.java @@ -3,60 +3,46 @@ import com.miguelbcr.ui.rx_paparazzo2.entities.Config; import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; import com.miguelbcr.ui.rx_paparazzo2.interactors.CropImage; -import com.miguelbcr.ui.rx_paparazzo2.interactors.DownloadImage; -import com.miguelbcr.ui.rx_paparazzo2.interactors.GetDimens; +import com.miguelbcr.ui.rx_paparazzo2.interactors.DownloadFile; +import com.miguelbcr.ui.rx_paparazzo2.interactors.ScaledImageDimensions; import com.miguelbcr.ui.rx_paparazzo2.interactors.GetPath; import com.miguelbcr.ui.rx_paparazzo2.interactors.GrantPermissions; import com.miguelbcr.ui.rx_paparazzo2.interactors.ImageUtils; -import com.miguelbcr.ui.rx_paparazzo2.interactors.PickImage; -import com.miguelbcr.ui.rx_paparazzo2.interactors.PickImages; -import com.miguelbcr.ui.rx_paparazzo2.interactors.SaveImage; +import com.miguelbcr.ui.rx_paparazzo2.interactors.SaveFile; import com.miguelbcr.ui.rx_paparazzo2.interactors.StartIntent; import com.miguelbcr.ui.rx_paparazzo2.interactors.TakePhoto; import com.miguelbcr.ui.rx_paparazzo2.workers.Camera; -import com.miguelbcr.ui.rx_paparazzo2.workers.Gallery; +import com.miguelbcr.ui.rx_paparazzo2.workers.Files; class ApplicationComponentImpl extends ApplicationComponent { - private final ImageUtils imageUtils; - private final DownloadImage downloadImage; - private final StartIntent startIntent; private final GetPath getPath; - private final GetDimens getDimens; - private final TakePhoto takePhoto; - private final CropImage cropImage; - private final SaveImage saveImage; - private final GrantPermissions grantPermissions; - private final PickImages pickImages; - private final PickImage pickImage; private final Camera camera; - private final Gallery gallery; + private final Files files; public ApplicationComponentImpl(TargetUi ui, Config config) { - startIntent = new StartIntent(ui); - imageUtils = new ImageUtils(ui, config); - downloadImage = new DownloadImage(ui, imageUtils); - getPath = new GetPath(ui, downloadImage); - takePhoto = new TakePhoto(startIntent, ui, imageUtils); - getDimens = new GetDimens(ui, config, getPath); - cropImage = new CropImage(ui, config, startIntent, getPath, imageUtils); - saveImage = new SaveImage(ui, getPath, getDimens, imageUtils); - grantPermissions = new GrantPermissions(ui); - pickImages = new PickImages(startIntent); - pickImage = new PickImage(startIntent, getPath); - camera = new Camera(takePhoto, cropImage, saveImage, grantPermissions, ui, config); - gallery = - new Gallery(grantPermissions, pickImages, pickImage, cropImage, saveImage, ui, config); + StartIntent startIntent = new StartIntent(ui); + ImageUtils imageUtils = new ImageUtils(ui, config); + DownloadFile downloadFile = new DownloadFile(ui, config, imageUtils); + TakePhoto takePhoto = new TakePhoto(config, startIntent, ui, imageUtils); + ScaledImageDimensions scaledImageDimensions = new ScaledImageDimensions(ui, config); + CropImage cropImage = new CropImage(ui, config, startIntent, imageUtils); + SaveFile saveFile = new SaveFile(ui, config, scaledImageDimensions, imageUtils); + GrantPermissions grantPermissions = new GrantPermissions(ui); + + this.getPath = new GetPath(config, ui, downloadFile); + this.camera = new Camera(takePhoto, cropImage, saveFile, grantPermissions, ui, config); + this.files = new Files(grantPermissions, startIntent, this.getPath, cropImage, saveFile, ui, config); } @Override public Camera camera() { return camera; } - @Override public Gallery gallery() { - return gallery; - } - @Override public GetPath getPath() { return getPath; } + + @Override public Files files() { + return files; + } } diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Camera.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Camera.java index c484828..dd75169 100644 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Camera.java +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Camera.java @@ -20,15 +20,20 @@ import android.app.Activity; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.net.Uri; + import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; import com.miguelbcr.ui.rx_paparazzo2.entities.Ignore; import com.miguelbcr.ui.rx_paparazzo2.entities.Response; import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; import com.miguelbcr.ui.rx_paparazzo2.interactors.CropImage; import com.miguelbcr.ui.rx_paparazzo2.interactors.GrantPermissions; -import com.miguelbcr.ui.rx_paparazzo2.interactors.SaveImage; +import com.miguelbcr.ui.rx_paparazzo2.interactors.PermissionUtil; +import com.miguelbcr.ui.rx_paparazzo2.interactors.SaveFile; import com.miguelbcr.ui.rx_paparazzo2.interactors.TakePhoto; + +import java.util.Arrays; + import io.reactivex.Observable; import io.reactivex.ObservableSource; import io.reactivex.functions.Function; @@ -36,76 +41,80 @@ public final class Camera extends Worker { private final TakePhoto takePhoto; private final CropImage cropImage; - private final SaveImage saveImage; + private final SaveFile saveFile; private final GrantPermissions grantPermissions; private final TargetUi targetUi; private final Config config; - public Camera(TakePhoto takePhoto, CropImage cropImage, SaveImage saveImage, + public Camera(TakePhoto takePhoto, CropImage cropImage, SaveFile saveFile, GrantPermissions grantPermissions, TargetUi targetUi, Config config) { super(targetUi); this.takePhoto = takePhoto; this.cropImage = cropImage; - this.saveImage = saveImage; + this.saveFile = saveFile; this.grantPermissions = grantPermissions; this.targetUi = targetUi; this.config = config; } - public Observable> takePhoto() { + public Observable> takePhoto() { return grantPermissions.with(permissions()) .react() - .flatMap(new Function>() { - @Override public ObservableSource apply(Ignore ignore) throws Exception { + .flatMap(new Function>() { + @Override public ObservableSource apply(Ignore ignore) throws Exception { return takePhoto.react(); } }) - .flatMap(new Function>() { - @Override public ObservableSource apply(Uri uri) throws Exception { - return cropImage.with(uri).react(); - } - }) - .flatMap(new Function>() { - @Override public ObservableSource apply(Uri uri) throws Exception { - return saveImage.with(uri).react(); + .flatMap(new Function>() { + @Override public ObservableSource apply(FileData fileData) throws Exception { + return handleSavingFile(fileData); } }) - .map(new Function>() { - @Override public Response apply(String path) throws Exception { - return new Response<>((T) targetUi.ui(), path, Activity.RESULT_OK); + .map(new Function>() { + @Override public Response apply(FileData fileData) throws Exception { + return new Response<>((T) targetUi.ui(), fileData, Activity.RESULT_OK); } }) - .compose(this.>applyOnError()); + .compose(this.>applyOnError()); + } + + private Observable handleSavingFile(final FileData sourceFileData) { + return cropImage.with(sourceFileData).react() + .flatMap(new Function>() { + public ObservableSource apply(FileData cropped) throws Exception { + return saveFile.with(cropped).react(); + } + }); } private String[] permissions() { - if (config.useInternalStorage()) { - if (hasCameraPermissionInManifest()) { - return new String[] { Manifest.permission.CAMERA }; - } else { - return new String[] {}; - } - } else { - if (hasCameraPermissionInManifest()) { - return new String[] { - Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE - }; - } else { - return new String[] { - Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE - }; - } + String[] storagePermission = PermissionUtil.getReadAndWriteStoragePermissions(config.isUseInternalStorage()); + String[] cameraPermission = getCameraPermission(); + + return concat(storagePermission, cameraPermission); + } + + private String[] getCameraPermission() { + if (hasCameraPermissionInManifest()) { + return new String[] { Manifest.permission.CAMERA }; } + + return new String[] {}; + } + + private static T[] concat(T[] first, T[] second) { + T[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + + return result; } private boolean hasCameraPermissionInManifest() { - final String packageName = targetUi.getContext().getPackageName(); try { - final PackageInfo packageInfo = targetUi.getContext() - .getPackageManager() - .getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); - final String[] declaredPermissions = packageInfo.requestedPermissions; + String packageName = targetUi.getContext().getPackageName(); + PackageManager pm = targetUi.getContext().getPackageManager(); + PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); + String[] declaredPermissions = packageInfo.requestedPermissions; if (declaredPermissions != null && declaredPermissions.length > 0) { for (String p : declaredPermissions) { if (p.equals(Manifest.permission.CAMERA)) { diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Files.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Files.java new file mode 100644 index 0000000..8d6e284 --- /dev/null +++ b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Files.java @@ -0,0 +1,151 @@ +/* + * Copyright 2016 Miguel Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.miguelbcr.ui.rx_paparazzo2.workers; + +import android.app.Activity; +import android.net.Uri; + +import com.miguelbcr.ui.rx_paparazzo2.entities.Config; +import com.miguelbcr.ui.rx_paparazzo2.entities.FileData; +import com.miguelbcr.ui.rx_paparazzo2.entities.Ignore; +import com.miguelbcr.ui.rx_paparazzo2.entities.Response; +import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; +import com.miguelbcr.ui.rx_paparazzo2.interactors.CropImage; +import com.miguelbcr.ui.rx_paparazzo2.interactors.GetPath; +import com.miguelbcr.ui.rx_paparazzo2.interactors.GrantPermissions; +import com.miguelbcr.ui.rx_paparazzo2.interactors.PermissionUtil; +import com.miguelbcr.ui.rx_paparazzo2.interactors.PickFile; +import com.miguelbcr.ui.rx_paparazzo2.interactors.PickFiles; +import com.miguelbcr.ui.rx_paparazzo2.interactors.SaveFile; +import com.miguelbcr.ui.rx_paparazzo2.interactors.StartIntent; + +import java.util.List; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.functions.Function; + +public final class Files extends Worker { + private final GrantPermissions grantPermissions; + private final StartIntent startIntent; + private final GetPath getPath; + private final CropImage cropImage; + private final SaveFile saveFile; + private final TargetUi targetUi; + private final Config config; + + public Files(GrantPermissions grantPermissions, StartIntent startIntent, GetPath getPath, + CropImage cropImage, SaveFile saveFile, TargetUi targetUi, Config config) { + super(targetUi); + this.grantPermissions = grantPermissions; + this.startIntent = startIntent; + this.getPath = getPath; + this.cropImage = cropImage; + this.saveFile = saveFile; + this.targetUi = targetUi; + this.config = config; + } + + public Observable> pickFile() { + PickFile pickFile = new PickFile(targetUi, config, startIntent); + + return pickFile(pickFile); + } + + public Observable>> pickFiles() { + PickFiles pickFiles = new PickFiles(targetUi, config, startIntent); + + return pickFiles(pickFiles); + } + + public Observable> pickFile(final PickFile pickFile) { + return grantPermissions.with(permissions()) + .react() + .flatMap(new Function>() { + @Override public ObservableSource apply(Ignore ignore) throws Exception { + return pickFile.react(); + } + }) + .flatMap(new Function>() { + @Override + public ObservableSource apply(final Uri uri) throws Exception { + return getPath.with(uri).react(); + } + }) + .flatMap(new Function>() { + @Override + public ObservableSource apply(FileData fileData) throws Exception { + return handleSavingFile(fileData); + } + }) + .map(new Function>() { + @Override public Response apply(FileData file) throws Exception { + return new Response<>((T) targetUi.ui(), file, Activity.RESULT_OK); + } + }) + .compose(this.>applyOnError()); + } + + private Observable handleSavingFile(final FileData sourceFileData) { + return cropImage.with(sourceFileData).react() + .flatMap(new Function>() { + @Override + public ObservableSource apply(FileData cropped) throws Exception { + return saveFile.with(cropped).react(); + } + }); + } + + public Observable>> pickFiles(final PickFiles pickFiles) { + return grantPermissions.with(permissions()) + .react() + .flatMap(new Function>>() { + @Override public ObservableSource> apply(Ignore ignore) throws Exception { + return pickFiles.react(); + } + }) + .flatMapIterable(new Function, Iterable>() { + @Override public Iterable apply(List uris) throws Exception { + return uris; + } + }) + .concatMap(new Function>() { + @Override + public ObservableSource apply(final Uri uri) throws Exception { + return getPath.with(uri).react(); + } + }) + .concatMap(new Function>() { + @Override + public ObservableSource apply(FileData fileData) throws Exception { + return handleSavingFile(fileData); + } + }) + .toList() + .toObservable() + .map(new Function, Response>>() { + @Override public Response> apply(List paths) throws Exception { + return new Response<>((T) targetUi.ui(), paths, Activity.RESULT_OK); + } + }) + .compose(this.>>applyOnError()); + } + + private String[] permissions() { + return PermissionUtil.getReadAndWriteStoragePermissions(config.isUseInternalStorage()); + } +} diff --git a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Gallery.java b/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Gallery.java deleted file mode 100644 index 5cbfe1a..0000000 --- a/rx_paparazzo/src/main/java/com/miguelbcr/ui/rx_paparazzo2/workers/Gallery.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2016 Miguel Garcia - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.miguelbcr.ui.rx_paparazzo2.workers; - -import android.Manifest; -import android.app.Activity; -import android.net.Uri; -import com.miguelbcr.ui.rx_paparazzo2.entities.Config; -import com.miguelbcr.ui.rx_paparazzo2.entities.Ignore; -import com.miguelbcr.ui.rx_paparazzo2.entities.Response; -import com.miguelbcr.ui.rx_paparazzo2.entities.TargetUi; -import com.miguelbcr.ui.rx_paparazzo2.interactors.CropImage; -import com.miguelbcr.ui.rx_paparazzo2.interactors.GrantPermissions; -import com.miguelbcr.ui.rx_paparazzo2.interactors.PickImage; -import com.miguelbcr.ui.rx_paparazzo2.interactors.PickImages; -import com.miguelbcr.ui.rx_paparazzo2.interactors.SaveImage; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.functions.Function; -import java.util.List; - -public final class Gallery extends Worker { - private final GrantPermissions grantPermissions; - private final PickImages pickImages; - private final PickImage pickImage; - private final CropImage cropImage; - private final SaveImage saveImage; - private final TargetUi targetUi; - private final Config config; - - public Gallery(GrantPermissions grantPermissions, PickImages pickImages, PickImage pickImage, - CropImage cropImage, SaveImage saveImage, TargetUi targetUi, Config config) { - super(targetUi); - this.grantPermissions = grantPermissions; - this.pickImages = pickImages; - this.pickImage = pickImage; - this.cropImage = cropImage; - this.saveImage = saveImage; - this.targetUi = targetUi; - this.config = config; - } - - public Observable> pickImage() { - return grantPermissions.with(permissions()) - .react() - .flatMap(new Function>() { - @Override public ObservableSource apply(Ignore ignore) throws Exception { - return pickImage.react(); - } - }) - .flatMap(new Function>() { - @Override public ObservableSource apply(Uri uri) throws Exception { - return cropImage.with(uri).react(); - } - }) - .flatMap(new Function>() { - @Override public ObservableSource apply(Uri uri) throws Exception { - return saveImage.with(uri).react(); - } - }) - .map(new Function>() { - @Override public Response apply(String path) throws Exception { - return new Response<>((T) targetUi.ui(), path, Activity.RESULT_OK); - } - }) - .compose(this.>applyOnError()); - } - - public Observable>> pickImages() { - return grantPermissions.with(permissions()) - .react() - .flatMap(new Function>>() { - @Override public ObservableSource> apply(Ignore ignore) throws Exception { - return pickImages.react(); - } - }) - .flatMapIterable(new Function, Iterable>() { - @Override public Iterable apply(List uris) throws Exception { - return uris; - } - }) - .concatMap(new Function>() { - @Override public ObservableSource apply(Uri uri) throws Exception { - return cropImage.with(uri).react(); - } - }) - .concatMap(new Function>() { - @Override public ObservableSource apply(Uri uri) throws Exception { - return saveImage.with(uri).react(); - } - }) - .toList() - .toObservable() - .map(new Function, Response>>() { - @Override public Response> apply(List paths) throws Exception { - return new Response<>((T) targetUi.ui(), paths, Activity.RESULT_OK); - } - }) - .compose(this.>>applyOnError()); - } - - private String[] permissions() { - if (config.useInternalStorage()) { - return new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }; - } else { - return new String[] { - Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE - }; - } - } -}