rhyme_lph 5 éve
commit
4a5514de6a
100 módosított fájl, 6540 hozzáadás és 0 törlés
  1. 462 0
      CHANGELOG.md
  2. 232 0
      LICENSE
  3. 158 0
      README.md
  4. 41 0
      android/build.gradle
  5. 1 0
      android/gradle.properties
  6. 1 0
      android/settings.gradle
  7. 17 0
      android/src/main/AndroidManifest.xml
  8. 55 0
      android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java
  9. 122 0
      android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java
  10. 165 0
      android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java
  11. 626 0
      android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
  12. 14 0
      android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java
  13. 325 0
      android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
  14. 43 0
      android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java
  15. 154 0
      android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java
  16. 4 0
      android/src/main/res/xml/flutter_image_picker_file_paths.xml
  17. 8 0
      example/README.md
  18. 12 0
      example/android.iml
  19. 68 0
      example/android/app/build.gradle
  20. 5 0
      example/android/app/gradle/wrapper/gradle-wrapper.properties
  21. 26 0
      example/android/app/src/main/AndroidManifest.xml
  22. 1 0
      example/android/app/src/main/java/io/flutter/plugins/.gitignore
  23. 21 0
      example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java
  24. 17 0
      example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java
  25. 13 0
      example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java
  26. BIN
      example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  27. BIN
      example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  28. BIN
      example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  29. BIN
      example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  30. BIN
      example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  31. 57 0
      example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java
  32. 115 0
      example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java
  33. 421 0
      example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
  34. 152 0
      example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
  35. 73 0
      example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java
  36. 1 0
      example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
  37. BIN
      example/android/app/src/test/resources/pngImage.png
  38. 29 0
      example/android/build.gradle
  39. 5 0
      example/android/gradle.properties
  40. 5 0
      example/android/gradle/wrapper/gradle-wrapper.properties
  41. 15 0
      example/android/settings.gradle
  42. 16 0
      example/image_picker_example.iml
  43. 30 0
      example/ios/Flutter/AppFrameworkInfo.plist
  44. 2 0
      example/ios/Flutter/Debug.xcconfig
  45. 2 0
      example/ios/Flutter/Release.xcconfig
  46. 668 0
      example/ios/Runner.xcodeproj/project.pbxproj
  47. 10 0
      example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  48. 97 0
      example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  49. 10 0
      example/ios/Runner.xcworkspace/contents.xcworkspacedata
  50. 8 0
      example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  51. 2 0
      example/ios/Runner/.gitignore
  52. 10 0
      example/ios/Runner/AppDelegate.h
  53. 16 0
      example/ios/Runner/AppDelegate.m
  54. 121 0
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  55. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  56. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  57. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  58. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  59. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  60. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  61. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  62. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  63. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  64. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  65. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  66. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  67. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  68. BIN
      example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  69. 6 0
      example/ios/Runner/Assets.xcassets/Contents.json
  70. 27 0
      example/ios/Runner/Base.lproj/LaunchScreen.storyboard
  71. 26 0
      example/ios/Runner/Base.lproj/Main.storyboard
  72. 59 0
      example/ios/Runner/Info.plist
  73. 13 0
      example/ios/Runner/main.m
  74. BIN
      example/ios/TestImages/gifImage.gif
  75. BIN
      example/ios/TestImages/jpgImage.jpg
  76. BIN
      example/ios/TestImages/pngImage.png
  77. 22 0
      example/ios/image_picker_exampleTests/Info.plist
  78. 395 0
      example/lib/main.dart
  79. 25 0
      example/pubspec.yaml
  80. 15 0
      example/test_driver/test/image_picker_e2e_test.dart
  81. BIN
      example/web/favicon.png
  82. BIN
      example/web/icons/Icon-192.png
  83. BIN
      example/web/icons/Icon-512.png
  84. 33 0
      example/web/index.html
  85. 23 0
      example/web/manifest.json
  86. 0 0
      ios/Assets/.gitkeep
  87. 32 0
      ios/Classes/FLTImagePickerImageUtil.h
  88. 147 0
      ios/Classes/FLTImagePickerImageUtil.m
  89. 44 0
      ios/Classes/FLTImagePickerMetaDataUtil.h
  90. 88 0
      ios/Classes/FLTImagePickerMetaDataUtil.m
  91. 31 0
      ios/Classes/FLTImagePickerPhotoAssetUtil.h
  92. 142 0
      ios/Classes/FLTImagePickerPhotoAssetUtil.m
  93. 13 0
      ios/Classes/FLTImagePickerPlugin.h
  94. 385 0
      ios/Classes/FLTImagePickerPlugin.m
  95. 152 0
      ios/Tests/ImagePickerPluginTests.m
  96. 17 0
      ios/Tests/ImagePickerTestImages.h
  97. 122 0
      ios/Tests/ImagePickerTestImages.m
  98. 41 0
      ios/Tests/ImageUtilTests.m
  99. 89 0
      ios/Tests/MetaDataUtilTests.m
  100. 137 0
      ios/Tests/PhotoAssetUtilTests.m

+ 462 - 0
CHANGELOG.md

@@ -0,0 +1,462 @@
+## 0.6.7+2
+
+* iOS: Fixes unpresentable album/image picker if window's root view controller is already presenting other view controller.
+
+## 0.6.7+1
+
+* Add web support to the example app.
+
+## 0.6.7
+
+* Utilize the new platform_interface package.
+* **This change marks old methods as `deprecated`. Please check the README for migration instructions to the new API.**
+
+## 0.6.6+5
+
+* Pin the version of the platform interface to 1.0.0 until the plugin refactor
+is ready to go.
+
+## 0.6.6+4
+
+* Fix bug, sometimes double click cancel button will crash.
+
+## 0.6.6+3
+
+* Update README
+
+## 0.6.6+2
+
+* Update lower bound of dart dependency to 2.1.0.
+
+## 0.6.6+1
+
+* Android: always use URI to get image/video data.
+
+## 0.6.6
+
+* Use the new platform_interface package.
+
+## 0.6.5+3
+
+* Move core plugin to a subdirectory to allow for federation.
+
+## 0.6.5+2
+
+* iOS: Fixes crash when an image in the gallery is tapped more than once.
+
+## 0.6.5+1
+
+* Fix CocoaPods podspec lint warnings.
+
+## 0.6.5
+
+* Set maximum duration for video recording.
+* Fix some existing XCTests.
+
+## 0.6.4
+
+* Add a new parameter to select preferred camera device.
+
+## 0.6.3+4
+
+* Make the pedantic dev_dependency explicit.
+
+## 0.6.3+3
+
+* Android: Fix a crash when `externalFilesDirectory` does not exist.
+
+## 0.6.3+2
+
+* Bump RoboElectric dependency to 4.3.1 and update resource usage.
+
+## 0.6.3+1
+
+* Fix an issue that the example app won't launch the image picker after Android V2 embedding migration.
+
+## 0.6.3
+
+* Support Android V2 embedding.
+* Migrate to using the new e2e test binding.
+
+## 0.6.2+3
+
+* Remove the deprecated `author:` field from pubspec.yaml
+* Migrate the plugin to the pubspec platforms manifest.
+* Require Flutter SDK 1.10.0 or greater.
+
+## 0.6.2+2
+
+* Android: Revert the image file return logic when the image doesn't have to be scaled. Fix a rotation regression caused by 0.6.2+1
+* Example App: Add a dialog to enter `maxWidth`, `maxHeight` or `quality` when picking image.
+
+## 0.6.2+1
+
+* Android: Fix a crash when a non-image file is picked.
+* Android: Fix unwanted bitmap scaling.
+
+## 0.6.2
+
+* iOS: Fixes an issue where picking content from Gallery would result in a crash on iOS 13.
+
+## 0.6.1+11
+
+* Stability and Maintainability: update documentations, add unit tests.
+
+## 0.6.1+10
+
+* iOS: Fix image orientation problems when scaling images.
+
+## 0.6.1+9
+
+* Remove AndroidX warning.
+
+## 0.6.1+8
+
+* Fix iOS build and analyzer warnings.
+
+## 0.6.1+7
+
+* Android: Fix ImagePickerPlugin#onCreate casting context which causes exception.
+
+## 0.6.1+6
+
+* Define clang module for iOS
+
+## 0.6.1+5
+
+* Update and migrate iOS example project.
+
+## 0.6.1+4
+
+* Android: Fix a regression where the `retrieveLostImage` does not work anymore.
+* Set up Android unit test to test `ImagePickerCache` and added image quality caching tests.
+
+## 0.6.1+3
+
+* Bugfix iOS: Fix orientation of the picked image after scaling.
+* Remove unnecessary code that tried to normalize the orientation.
+* Trivial XCTest code fix.
+
+## 0.6.1+2
+
+* Replace dependency on `androidx.legacy:legacy-support-v4:1.0.0` with `androidx.core:core:1.0.2`
+
+## 0.6.1+1
+
+* Add dependency on `androidx.annotation:annotation:1.0.0`.
+
+## 0.6.1
+
+* New feature : Get images with custom quality. While picking images, user can pass `imageQuality`
+parameter to compress image.
+
+## 0.6.0+20
+
+* Android: Migrated information cache methods to use instance methods.
+
+## 0.6.0+19
+
+* Android: Fix memory leak due not unregistering ActivityLifecycleCallbacks.
+
+## 0.6.0+18
+
+* Fix video play in example and update video_player plugin dependency.
+
+## 0.6.0+17
+
+* iOS: Fix a crash when user captures image from the camera with devices under iOS 11.
+
+## 0.6.0+16
+
+* iOS Simulator: fix hang after trying to take an image from the non-existent camera.
+
+## 0.6.0+15
+
+* Android: throws an exception when permissions denied instead of ignoring.
+
+## 0.6.0+14
+
+* Fix typo in README.
+
+## 0.6.0+13
+
+* Bugfix Android: Fix a crash occurs in some scenarios when user picks up image from gallery.
+
+## 0.6.0+12
+
+* Use class instead of struct for `GIFInfo` in iOS implementation.
+
+## 0.6.0+11
+
+* Don't use module imports.
+
+## 0.6.0+10
+
+* iOS: support picking GIF from gallery.
+
+## 0.6.0+9
+
+* Add missing template type parameter to `invokeMethod` calls.
+* Bump minimum Flutter version to 1.5.0.
+* Replace invokeMethod with invokeMapMethod wherever necessary.
+
+## 0.6.0+8
+
+* Bugfix: Add missed return statement into the image_picker example.
+
+## 0.6.0+7
+
+* iOS: Rename objects to follow Objective-C naming convention to avoid conflicts with other iOS library/frameworks.
+
+## 0.6.0+6
+
+* iOS: Picked image now has all the correct meta data from the original image, includes GPS, orientation and etc.
+
+## 0.6.0+5
+
+* iOS: Add missing import.
+
+## 0.6.0+4
+
+* iOS: Using first byte to determine original image type.
+* iOS: Added XCTest target.
+* iOS: The picked image now has the correct EXIF data copied from the original image.
+
+## 0.6.0+3
+
+* Android: fixed assertion failures due to reply messages that were sent on the wrong thread.
+
+## 0.6.0+2
+
+* Android: images are saved with their real extension instead of always using `.jpg`.
+
+## 0.6.0+1
+
+* Android: Using correct suffix syntax when picking image from remote url.
+
+## 0.6.0
+
+* Breaking change iOS: Returned `File` objects when picking videos now always holds the correct path. Before this change, the path returned could have `file://` prepended to it.
+
+## 0.5.4+3
+
+* Fix the example app failing to load picked video.
+
+## 0.5.4+2
+
+* Request Camera permission if it present in Manifest on Android >= M.
+
+## 0.5.4+1
+
+* Bugfix iOS: Cancel button not visible in gallery, if camera was accessed first.
+
+## 0.5.4
+
+* Add `retrieveLostData` to retrieve lost data after MainActivity is killed.
+
+## 0.5.3+2
+
+* Android: fix a crash when the MainActivity is destroyed after selecting the image/video.
+
+## 0.5.3+1
+
+* Update minimum deploy iOS version to 8.0.
+
+## 0.5.3
+
+* Fixed incorrect path being returned from Google Photos on Android.
+
+## 0.5.2
+
+* Check iOS camera authorizationStatus and return an error, if the access was
+  denied.
+
+## 0.5.1
+
+* Android: Do not delete original image after scaling if the image is from gallery.
+
+## 0.5.0+9
+
+* Remove unnecessary temp video file path.
+
+## 0.5.0+8
+
+* Fixed wrong GooglePhotos authority of image Uri.
+
+## 0.5.0+7
+
+* Fix a crash when selecting images from yandex.disk and dropbox.
+
+## 0.5.0+6
+
+* Delete the original image if it was scaled.
+
+## 0.5.0+5
+
+* Remove unnecessary camera permission.
+
+## 0.5.0+4
+
+* Preserve transparency when saving images.
+
+## 0.5.0+3
+
+* Fixed an Android crash when Image Picker is registered without an activity.
+
+## 0.5.0+2
+
+* Log a more detailed warning at build time about the previous AndroidX
+  migration.
+
+## 0.5.0+1
+
+* Fix a crash when user calls the plugin in quick succession on Android.
+
+## 0.5.0
+
+* **Breaking change**. Migrate from the deprecated original Android Support
+  Library to AndroidX. This shouldn't result in any functional changes, but it
+  requires any Android apps using this plugin to [also
+  migrate](https://developer.android.com/jetpack/androidx/migrate) if they're
+  using the original support library.
+
+## 0.4.12+1
+
+* Fix a crash when selecting downloaded images from image picker on certain devices.
+
+## 0.4.12
+
+* Fix a crash when user tap the image mutiple times.
+
+## 0.4.11
+
+* Use `api` to define `support-v4` dependency to allow automatic version resolution.
+
+## 0.4.10
+
+* Depend on full `support-v4` library for ease of use (fixes conflicts with Firebase and libraries)
+
+## 0.4.9
+
+* Bugfix: on iOS prevent to appear one pixel white line on resized image.
+
+## 0.4.8
+
+* Replace the full `com.android.support:appcompat-v7` dependency with `com.android.support:support-core-utils`, which results in smaller APK sizes.
+* Upgrade support library to 27.1.1
+
+## 0.4.7
+
+* Added missing video_player package dev dependency.
+
+## 0.4.6
+
+* Added support for picking remote images.
+
+## 0.4.5
+
+* Bugfixes, code cleanup, more test coverage.
+
+## 0.4.4
+
+* Updated Gradle tooling to match Android Studio 3.1.2.
+
+## 0.4.3
+
+* Bugfix: on iOS the `pickVideo` method will now return null when the user cancels picking a video.
+
+## 0.4.2
+
+* Added support for picking videos.
+* Updated example app to show video preview.
+
+## 0.4.1
+
+* Bugfix: the `pickImage` method will now return null when the user cancels picking the image, instead of hanging indefinitely.
+* Removed the third party library dependency for taking pictures with the camera.
+
+## 0.4.0
+
+* **Breaking change**. The `source` parameter for the `pickImage` is now required. Also, the `ImageSource.any` option doesn't exist anymore.
+* Use the native Android image gallery for picking images instead of a custom UI.
+
+## 0.3.1
+
+* Bugfix: Android version correctly asks for runtime camera permission when using `ImageSource.camera`.
+
+## 0.3.0
+
+* **Breaking change**. Set SDK constraints to match the Flutter beta release.
+
+## 0.2.1
+
+* Simplified and upgraded Android project template to Android SDK 27.
+* Updated package description.
+
+## 0.2.0
+
+* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin
+  3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in
+  order to use this version of the plugin. Instructions can be found
+  [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1).
+
+## 0.1.5
+
+* Added FLT prefix to iOS types
+
+## 0.1.4
+
+* Bugfix: canceling image picking threw exception.
+* Bugfix: errors in plugin state management.
+
+## 0.1.3
+
+* Added optional source argument to pickImage for controlling where the image comes from.
+
+## 0.1.2
+
+* Added optional maxWidth and maxHeight arguments to pickImage.
+
+## 0.1.1
+
+* Updated Gradle repositories declaration to avoid the need for manual configuration
+  in the consuming app.
+
+## 0.1.0+1
+
+* Updated readme and description in pubspec.yaml
+
+## 0.1.0
+
+* Updated dependencies
+* **Breaking Change**: You need to add a maven section with the "https://maven.google.com" endpoint to the repository section of your `android/build.gradle`. For example:
+```gradle
+allprojects {
+    repositories {
+        jcenter()
+        maven {                              // NEW
+            url "https://maven.google.com"   // NEW
+        }                                    // NEW
+    }
+}
+```
+
+## 0.0.3
+
+* Fix for crash on iPad when showing the Camera/Gallery selection dialog
+
+## 0.0.2+2
+
+* Updated README
+
+## 0.0.2+1
+
+* Updated README
+
+## 0.0.2
+
+* Fix crash when trying to access camera on a device without camera (e.g. the Simulator)
+
+## 0.0.1
+
+* Initial Release

+ 232 - 0
LICENSE

@@ -0,0 +1,232 @@
+image_picker
+
+Copyright 2017, the Flutter project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+aFileChooser
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2011 - 2013 Paul Burke
+
+   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.

+ 158 - 0
README.md

@@ -0,0 +1,158 @@
+# Image Picker plugin for Flutter
+
+[![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dartlang.org/packages/image_picker)
+
+A Flutter plugin for iOS and Android for picking images from the image library,
+and taking new pictures with the camera.
+
+## Installation
+
+First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+
+### iOS
+
+Add the following keys to your _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
+
+* `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor.
+* `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor.
+* `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor.
+
+### Android
+
+#### API 29+
+No configuration required - the plugin should work out of the box.
+
+#### API < 29
+
+Add `android:requestLegacyExternalStorage="true"` as an attribute to the `<application>` tag in AndroidManifest.xml. The [attribute](https://developer.android.com/training/data-storage/compatibility) is `false` by default on apps targeting Android Q. 
+
+### Example
+
+``` dart
+import 'package:image_picker/image_picker.dart';
+
+class MyHomePage extends StatefulWidget {
+  @override
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  File _image;
+  final picker = ImagePicker();
+
+  Future getImage() async {
+    final pickedFile = await picker.getImage(source: ImageSource.camera);
+
+    setState(() {
+      _image = File(pickedFile.path);
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Image Picker Example'),
+      ),
+      body: Center(
+        child: _image == null
+            ? Text('No image selected.')
+            : Image.file(_image),
+      ),
+      floatingActionButton: FloatingActionButton(
+        onPressed: getImage,
+        tooltip: 'Pick Image',
+        child: Icon(Icons.add_a_photo),
+      ),
+    );
+  }
+}
+```
+
+### Handling MainActivity destruction on Android
+
+Android system -- although very rarely -- sometimes kills the MainActivity after the image_picker finishes. When this happens, we lost the data selected from the image_picker. You can use `retrieveLostData` to retrieve the lost data in this situation. For example:
+
+```dart
+Future<void> retrieveLostData() async {
+  final LostData response =
+      await picker.getLostData();
+  if (response == null) {
+    return;
+  }
+  if (response.file != null) {
+    setState(() {
+      if (response.type == RetrieveType.video) {
+        _handleVideo(response.file);
+      } else {
+        _handleImage(response.file);
+      }
+    });
+  } else {
+    _handleError(response.exception);
+  }
+}
+```
+
+There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it.
+
+## Deprecation warnings in `pickImage`, `pickVideo` and `LostDataResponse`
+
+Starting with version **0.6.7** of the image_picker plugin, the API of the plugin changed slightly to allow for web implementations to exist.
+
+The **old methods that returned `dart:io` File objects were marked as deprecated**, and a new set of methods that return [`PickedFile` objects](https://pub.dev/documentation/image_picker_platform_interface/latest/image_picker_platform_interface/PickedFile-class.html) were introduced.
+
+### How to migrate from to ^0.6.7
+
+#### Instantiate the `ImagePicker`
+
+The new ImagePicker API does not rely in static methods anymore, so the first thing you'll need to do is to create a new instance of the plugin where you need it:
+
+```dart
+final _picker = ImagePicker();
+```
+
+#### Call the new methods
+
+The new methods **receive the same parameters as before**, but they **return a `PickedFile`, instead of a `File`**. The `LostDataResponse` class has been replaced by the [`LostData` class](https://pub.dev/documentation/image_picker_platform_interface/latest/image_picker_platform_interface/LostData-class.html).
+
+| Old API | New API |
+|---------|---------|
+| `File image = await ImagePicker.pickImage(...)` | `PickedFile image = await _picker.getImage(...)` |
+| `File video = await ImagePicker.pickVideo(...)` | `PickedFile video = await _picker.getVideo(...)` |
+| `LostDataResponse response = await ImagePicker.retrieveLostData()` | `LostData response = await _picker.getLostData()` |
+
+#### `PickedFile` to `File`
+
+If your app needs dart:io `File` objects to operate, you may transform `PickedFile` to `File` like so:
+
+```dart
+final pickedFile = await _picker.getImage(...);
+final File file = File(pickedFile.path);
+```
+
+You may also retrieve the bytes from the pickedFile directly if needed:
+
+```dart
+final bytes = await pickedFile.readAsBytes();
+```
+
+#### Getting ready for the web platform
+
+Note that on the web platform (`kIsWeb == true`), `File` is not available, so the `path` of the `PickedFile` will point to a network resource instead:
+
+```dart
+if (kIsWeb) {
+  image = Image.network(pickedFile.path);
+} else {
+  image = Image.file(File(pickedFile.path));
+}
+```
+
+Alternatively, the code may be unified at the expense of memory utilization:
+
+```dart
+image = Image.memory(await pickedFile.readAsBytes())
+```
+
+Take a look at the changes to the `example` app introduced in version 0.6.7 to see the migration steps applied there.

+ 41 - 0
android/build.gradle

@@ -0,0 +1,41 @@
+group 'io.flutter.plugins.imagepicker'
+version '1.0-SNAPSHOT'
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.3.0'
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        jcenter()
+        maven {
+            url 'https://google.bintray.com/exoplayer/'
+        }
+    }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        minSdkVersion 16
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+    dependencies {
+        implementation 'androidx.core:core:1.0.2'
+        implementation 'androidx.annotation:annotation:1.0.0'
+    }
+}

+ 1 - 0
android/gradle.properties

@@ -0,0 +1 @@
+org.gradle.jvmargs=-Xmx1536M

+ 1 - 0
android/settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'imagepicker'

+ 17 - 0
android/src/main/AndroidManifest.xml

@@ -0,0 +1,17 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="io.flutter.plugins.imagepicker">
+   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+    <application>
+        <provider
+            android:name="io.flutter.plugins.imagepicker.ImagePickerFileProvider"
+            android:authorities="${applicationId}.flutter.image_provider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/flutter_image_picker_file_paths"/>
+        </provider>
+    </application>
+</manifest>

+ 55 - 0
android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java

@@ -0,0 +1,55 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import android.media.ExifInterface;
+import android.util.Log;
+import java.util.Arrays;
+import java.util.List;
+
+class ExifDataCopier {
+  void copyExif(String filePathOri, String filePathDest) {
+    try {
+      ExifInterface oldExif = new ExifInterface(filePathOri);
+      ExifInterface newExif = new ExifInterface(filePathDest);
+
+      List<String> attributes =
+          Arrays.asList(
+              "FNumber",
+              "ExposureTime",
+              "ISOSpeedRatings",
+              "GPSAltitude",
+              "GPSAltitudeRef",
+              "FocalLength",
+              "GPSDateStamp",
+              "WhiteBalance",
+              "GPSProcessingMethod",
+              "GPSTimeStamp",
+              "DateTime",
+              "Flash",
+              "GPSLatitude",
+              "GPSLatitudeRef",
+              "GPSLongitude",
+              "GPSLongitudeRef",
+              "Make",
+              "Model",
+              "Orientation");
+      for (String attribute : attributes) {
+        setIfNotNull(oldExif, newExif, attribute);
+      }
+
+      newExif.saveAttributes();
+
+    } catch (Exception ex) {
+      Log.e("ExifDataCopier", "Error preserving Exif data on selected image: " + ex);
+    }
+  }
+
+  private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) {
+    if (oldExif.getAttribute(property) != null) {
+      newExif.setAttribute(property, oldExif.getAttribute(property));
+    }
+  }
+}

+ 122 - 0
android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java

@@ -0,0 +1,122 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/*
+ * Copyright (C) 2007-2008 OpenIntents.org
+ *
+ * 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.
+ *
+ * This file was modified by the Flutter authors from the following original file:
+ * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
+ */
+
+package io.flutter.plugins.imagepicker;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class FileUtils {
+
+    String getPathFromUri(final Context context, final Uri uri, final boolean isVideo) {
+        File file = null;
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+        boolean success = false;
+        try {
+            String extension = getImageExtension(context, uri, isVideo);
+            inputStream = context.getContentResolver().openInputStream(uri);
+            file = File.createTempFile("image_picker", extension, context.getCacheDir());
+            file.deleteOnExit();
+            outputStream = new FileOutputStream(file);
+            if (inputStream != null) {
+                copy(inputStream, outputStream);
+                success = true;
+            }
+        } catch (IOException ignored) {
+        } finally {
+            try {
+                if (inputStream != null) inputStream.close();
+            } catch (IOException ignored) {
+            }
+            try {
+                if (outputStream != null) outputStream.close();
+            } catch (IOException ignored) {
+                // If closing the output stream fails, we cannot be sure that the
+                // target file was written in full. Flushing the stream merely moves
+                // the bytes into the OS, not necessarily to the file.
+                success = false;
+            }
+        }
+        return success ? file.getPath() : null;
+    }
+
+    /**
+     * @return extension of image with dot, or default .jpg if it none.
+     */
+    private static String getImageExtension(Context context, Uri uriImage, boolean isVideo) {
+        String extension = null;
+        try {
+            String imagePath = getPath(context, uriImage);
+            if (imagePath != null && imagePath.lastIndexOf(".") != -1) {
+                extension = imagePath.substring(imagePath.lastIndexOf(".") + 1);
+            }
+        } catch (Exception e) {
+            extension = null;
+        }
+
+        if (extension == null || extension.isEmpty()) {
+            //default extension for matches the previous behavior of the plugin
+            if (isVideo) {
+                extension = "mp4";
+            } else {
+                extension = "jpg";
+            }
+        }
+
+        return "." + extension;
+    }
+
+    private static String getPath(Context context, Uri uriImage) {
+        if ("content".equalsIgnoreCase(uriImage.getScheme())) {
+            if (uriImage == null) return null;
+            Cursor cursor = context.getContentResolver().query(uriImage, null, null, null, null);
+            cursor.moveToFirst();
+            int columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+            String filePath = cursor.getString(columnIndex);
+            cursor.close();
+            return filePath;
+        } else if ("file".equalsIgnoreCase(uriImage.getScheme())) {
+            return uriImage.getPath();
+        }
+        return uriImage.getPath();
+    }
+
+    private static void copy(InputStream in, OutputStream out) throws IOException {
+        final byte[] buffer = new byte[4 * 1024];
+        int bytesRead;
+        while ((bytesRead = in.read(buffer)) != -1) {
+            out.write(buffer, 0, bytesRead);
+        }
+        out.flush();
+    }
+}

+ 165 - 0
android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java

@@ -0,0 +1,165 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import io.flutter.plugin.common.MethodCall;
+import java.util.HashMap;
+import java.util.Map;
+
+class ImagePickerCache {
+
+  static final String MAP_KEY_PATH = "path";
+  static final String MAP_KEY_MAX_WIDTH = "maxWidth";
+  static final String MAP_KEY_MAX_HEIGHT = "maxHeight";
+  static final String MAP_KEY_IMAGE_QUALITY = "imageQuality";
+  private static final String MAP_KEY_TYPE = "type";
+  private static final String MAP_KEY_ERROR_CODE = "errorCode";
+  private static final String MAP_KEY_ERROR_MESSAGE = "errorMessage";
+
+  private static final String FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY =
+      "flutter_image_picker_image_path";
+  private static final String SHARED_PREFERENCE_ERROR_CODE_KEY = "flutter_image_picker_error_code";
+  private static final String SHARED_PREFERENCE_ERROR_MESSAGE_KEY =
+      "flutter_image_picker_error_message";
+
+  private static final String SHARED_PREFERENCE_MAX_WIDTH_KEY = "flutter_image_picker_max_width";
+
+  private static final String SHARED_PREFERENCE_MAX_HEIGHT_KEY = "flutter_image_picker_max_height";
+
+  private static final String SHARED_PREFERENCE_IMAGE_QUALITY_KEY =
+      "flutter_image_picker_image_quality";
+
+  private static final String SHARED_PREFERENCE_TYPE_KEY = "flutter_image_picker_type";
+  private static final String SHARED_PREFERENCE_PENDING_IMAGE_URI_PATH_KEY =
+      "flutter_image_picker_pending_image_uri";
+
+  @VisibleForTesting
+  static final String SHARED_PREFERENCES_NAME = "flutter_image_picker_shared_preference";
+
+  private SharedPreferences prefs;
+
+  ImagePickerCache(Context context) {
+    prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+  }
+
+  void saveTypeWithMethodCallName(String methodCallName) {
+    if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_IMAGE)) {
+      setType("image");
+    } else if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_VIDEO)) {
+      setType("video");
+    }
+  }
+
+  private void setType(String type) {
+
+    prefs.edit().putString(SHARED_PREFERENCE_TYPE_KEY, type).apply();
+  }
+
+  void saveDimensionWithMethodCall(MethodCall methodCall) {
+    Double maxWidth = methodCall.argument(MAP_KEY_MAX_WIDTH);
+    Double maxHeight = methodCall.argument(MAP_KEY_MAX_HEIGHT);
+    int imageQuality =
+        methodCall.argument(MAP_KEY_IMAGE_QUALITY) == null
+            ? 100
+            : (int) methodCall.argument(MAP_KEY_IMAGE_QUALITY);
+
+    setMaxDimension(maxWidth, maxHeight, imageQuality);
+  }
+
+  private void setMaxDimension(Double maxWidth, Double maxHeight, int imageQuality) {
+    SharedPreferences.Editor editor = prefs.edit();
+    if (maxWidth != null) {
+      editor.putLong(SHARED_PREFERENCE_MAX_WIDTH_KEY, Double.doubleToRawLongBits(maxWidth));
+    }
+    if (maxHeight != null) {
+      editor.putLong(SHARED_PREFERENCE_MAX_HEIGHT_KEY, Double.doubleToRawLongBits(maxHeight));
+    }
+    if (imageQuality > -1 && imageQuality < 101) {
+      editor.putInt(SHARED_PREFERENCE_IMAGE_QUALITY_KEY, imageQuality);
+    } else {
+      editor.putInt(SHARED_PREFERENCE_IMAGE_QUALITY_KEY, 100);
+    }
+    editor.apply();
+  }
+
+  void savePendingCameraMediaUriPath(Uri uri) {
+    prefs.edit().putString(SHARED_PREFERENCE_PENDING_IMAGE_URI_PATH_KEY, uri.getPath()).apply();
+  }
+
+  String retrievePendingCameraMediaUriPath() {
+
+    return prefs.getString(SHARED_PREFERENCE_PENDING_IMAGE_URI_PATH_KEY, "");
+  }
+
+  void saveResult(
+      @Nullable String path, @Nullable String errorCode, @Nullable String errorMessage) {
+
+    SharedPreferences.Editor editor = prefs.edit();
+    if (path != null) {
+      editor.putString(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, path);
+    }
+    if (errorCode != null) {
+      editor.putString(SHARED_PREFERENCE_ERROR_CODE_KEY, errorCode);
+    }
+    if (errorMessage != null) {
+      editor.putString(SHARED_PREFERENCE_ERROR_MESSAGE_KEY, errorMessage);
+    }
+    editor.apply();
+  }
+
+  void clear() {
+    prefs.edit().clear().apply();
+  }
+
+  Map<String, Object> getCacheMap() {
+
+    Map<String, Object> resultMap = new HashMap<>();
+    boolean hasData = false;
+
+    if (prefs.contains(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY)) {
+      final String imagePathValue = prefs.getString(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, "");
+      resultMap.put(MAP_KEY_PATH, imagePathValue);
+      hasData = true;
+    }
+
+    if (prefs.contains(SHARED_PREFERENCE_ERROR_CODE_KEY)) {
+      final String errorCodeValue = prefs.getString(SHARED_PREFERENCE_ERROR_CODE_KEY, "");
+      resultMap.put(MAP_KEY_ERROR_CODE, errorCodeValue);
+      hasData = true;
+      if (prefs.contains(SHARED_PREFERENCE_ERROR_MESSAGE_KEY)) {
+        final String errorMessageValue = prefs.getString(SHARED_PREFERENCE_ERROR_MESSAGE_KEY, "");
+        resultMap.put(MAP_KEY_ERROR_MESSAGE, errorMessageValue);
+      }
+    }
+
+    if (hasData) {
+      if (prefs.contains(SHARED_PREFERENCE_TYPE_KEY)) {
+        final String typeValue = prefs.getString(SHARED_PREFERENCE_TYPE_KEY, "");
+        resultMap.put(MAP_KEY_TYPE, typeValue);
+      }
+      if (prefs.contains(SHARED_PREFERENCE_MAX_WIDTH_KEY)) {
+        final long maxWidthValue = prefs.getLong(SHARED_PREFERENCE_MAX_WIDTH_KEY, 0);
+        resultMap.put(MAP_KEY_MAX_WIDTH, Double.longBitsToDouble(maxWidthValue));
+      }
+      if (prefs.contains(SHARED_PREFERENCE_MAX_HEIGHT_KEY)) {
+        final long maxHeightValue = prefs.getLong(SHARED_PREFERENCE_MAX_HEIGHT_KEY, 0);
+        resultMap.put(MAP_KEY_MAX_HEIGHT, Double.longBitsToDouble(maxHeightValue));
+      }
+      if (prefs.contains(SHARED_PREFERENCE_IMAGE_QUALITY_KEY)) {
+        final int imageQuality = prefs.getInt(SHARED_PREFERENCE_IMAGE_QUALITY_KEY, 100);
+        resultMap.put(MAP_KEY_IMAGE_QUALITY, imageQuality);
+      } else {
+        resultMap.put(MAP_KEY_IMAGE_QUALITY, 100);
+      }
+    }
+
+    return resultMap;
+  }
+}

+ 626 - 0
android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java

@@ -0,0 +1,626 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.hardware.camera2.CameraCharacteristics;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.FileProvider;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+enum CameraDevice {
+  REAR,
+
+  FRONT
+}
+
+/**
+ * A delegate class doing the heavy lifting for the plugin.
+ *
+ * <p>When invoked, both the {@link #chooseImageFromGallery} and {@link #takeImageWithCamera}
+ * methods go through the same steps:
+ *
+ * <p>1. Check for an existing {@link #pendingResult}. If a previous pendingResult exists, this
+ * means that the chooseImageFromGallery() or takeImageWithCamera() method was called at least
+ * twice. In this case, stop executing and finish with an error.
+ *
+ * <p>2. Check that a required runtime permission has been granted. The chooseImageFromGallery()
+ * method checks if the {@link Manifest.permission#READ_EXTERNAL_STORAGE} permission has been
+ * granted. Similarly, the takeImageWithCamera() method checks that {@link
+ * Manifest.permission#CAMERA} has been granted.
+ *
+ * <p>The permission check can end up in two different outcomes:
+ *
+ * <p>A) If the permission has already been granted, continue with picking the image from gallery or
+ * camera.
+ *
+ * <p>B) If the permission hasn't already been granted, ask for the permission from the user. If the
+ * user grants the permission, proceed with step #3. If the user denies the permission, stop doing
+ * anything else and finish with a null result.
+ *
+ * <p>3. Launch the gallery or camera for picking the image, depending on whether
+ * chooseImageFromGallery() or takeImageWithCamera() was called.
+ *
+ * <p>This can end up in three different outcomes:
+ *
+ * <p>A) User picks an image. No maxWidth or maxHeight was specified when calling {@code
+ * pickImage()} method in the Dart side of this plugin. Finish with full path for the picked image
+ * as the result.
+ *
+ * <p>B) User picks an image. A maxWidth and/or maxHeight was provided when calling {@code
+ * pickImage()} method in the Dart side of this plugin. A scaled copy of the image is created.
+ * Finish with full path for the scaled image as the result.
+ *
+ * <p>C) User cancels picking an image. Finish with null result.
+ */
+public class ImagePickerDelegate
+    implements PluginRegistry.ActivityResultListener,
+        PluginRegistry.RequestPermissionsResultListener {
+  @VisibleForTesting static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY = 2342;
+  @VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343;
+  @VisibleForTesting static final int REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION = 2344;
+  @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345;
+  @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352;
+  @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
+  @VisibleForTesting static final int REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION = 2354;
+  @VisibleForTesting static final int REQUEST_CAMERA_VIDEO_PERMISSION = 2355;
+
+  @VisibleForTesting final String fileProviderName;
+
+  private final Activity activity;
+  private final File externalFilesDirectory;
+  private final ImageResizer imageResizer;
+  private final ImagePickerCache cache;
+  private final PermissionManager permissionManager;
+  private final IntentResolver intentResolver;
+  private final FileUriResolver fileUriResolver;
+  private final FileUtils fileUtils;
+  private CameraDevice cameraDevice;
+
+  interface PermissionManager {
+    boolean isPermissionGranted(String permissionName);
+
+    void askForPermission(String permissionName, int requestCode);
+
+    boolean needRequestCameraPermission();
+  }
+
+  interface IntentResolver {
+    boolean resolveActivity(Intent intent);
+  }
+
+  interface FileUriResolver {
+    Uri resolveFileProviderUriForFile(String fileProviderName, File imageFile);
+
+    void getFullImagePath(Uri imageUri, OnPathReadyListener listener);
+  }
+
+  interface OnPathReadyListener {
+    void onPathReady(String path);
+  }
+
+  private Uri pendingCameraMediaUri;
+  private MethodChannel.Result pendingResult;
+  private MethodCall methodCall;
+
+  public ImagePickerDelegate(
+      final Activity activity,
+      final File externalFilesDirectory,
+      final ImageResizer imageResizer,
+      final ImagePickerCache cache) {
+    this(
+        activity,
+        externalFilesDirectory,
+        imageResizer,
+        null,
+        null,
+        cache,
+        new PermissionManager() {
+          @Override
+          public boolean isPermissionGranted(String permissionName) {
+            return ActivityCompat.checkSelfPermission(activity, permissionName)
+                == PackageManager.PERMISSION_GRANTED;
+          }
+
+          @Override
+          public void askForPermission(String permissionName, int requestCode) {
+            ActivityCompat.requestPermissions(activity, new String[] {permissionName}, requestCode);
+          }
+
+          @Override
+          public boolean needRequestCameraPermission() {
+            return ImagePickerUtils.needRequestCameraPermission(activity);
+          }
+        },
+        new IntentResolver() {
+          @Override
+          public boolean resolveActivity(Intent intent) {
+            return intent.resolveActivity(activity.getPackageManager()) != null;
+          }
+        },
+        new FileUriResolver() {
+          @Override
+          public Uri resolveFileProviderUriForFile(String fileProviderName, File file) {
+            return FileProvider.getUriForFile(activity, fileProviderName, file);
+          }
+
+          @Override
+          public void getFullImagePath(final Uri imageUri, final OnPathReadyListener listener) {
+            MediaScannerConnection.scanFile(
+                activity,
+                new String[] {(imageUri != null) ? imageUri.getPath() : ""},
+                null,
+                new MediaScannerConnection.OnScanCompletedListener() {
+                  @Override
+                  public void onScanCompleted(String path, Uri uri) {
+                    listener.onPathReady(path);
+                  }
+                });
+          }
+        },
+        new FileUtils());
+  }
+
+  /**
+   * This constructor is used exclusively for testing; it can be used to provide mocks to final
+   * fields of this class. Otherwise those fields would have to be mutable and visible.
+   */
+  @VisibleForTesting
+  ImagePickerDelegate(
+      final Activity activity,
+      final File externalFilesDirectory,
+      final ImageResizer imageResizer,
+      final MethodChannel.Result result,
+      final MethodCall methodCall,
+      final ImagePickerCache cache,
+      final PermissionManager permissionManager,
+      final IntentResolver intentResolver,
+      final FileUriResolver fileUriResolver,
+      final FileUtils fileUtils) {
+    this.activity = activity;
+    this.externalFilesDirectory = externalFilesDirectory;
+    this.imageResizer = imageResizer;
+    this.fileProviderName = activity.getPackageName() + ".flutter.image_provider";
+    this.pendingResult = result;
+    this.methodCall = methodCall;
+    this.permissionManager = permissionManager;
+    this.intentResolver = intentResolver;
+    this.fileUriResolver = fileUriResolver;
+    this.fileUtils = fileUtils;
+    this.cache = cache;
+  }
+
+  void setCameraDevice(CameraDevice device) {
+    cameraDevice = device;
+  }
+
+  CameraDevice getCameraDevice() {
+    return cameraDevice;
+  }
+
+  // Save the state of the image picker so it can be retrieved with `retrieveLostImage`.
+  void saveStateBeforeResult() {
+    if (methodCall == null) {
+      return;
+    }
+
+    cache.saveTypeWithMethodCallName(methodCall.method);
+    cache.saveDimensionWithMethodCall(methodCall);
+    if (pendingCameraMediaUri != null) {
+      cache.savePendingCameraMediaUriPath(pendingCameraMediaUri);
+    }
+  }
+
+  void retrieveLostImage(MethodChannel.Result result) {
+    Map<String, Object> resultMap = cache.getCacheMap();
+    String path = (String) resultMap.get(cache.MAP_KEY_PATH);
+    if (path != null) {
+      Double maxWidth = (Double) resultMap.get(cache.MAP_KEY_MAX_WIDTH);
+      Double maxHeight = (Double) resultMap.get(cache.MAP_KEY_MAX_HEIGHT);
+      int imageQuality =
+          resultMap.get(cache.MAP_KEY_IMAGE_QUALITY) == null
+              ? 100
+              : (int) resultMap.get(cache.MAP_KEY_IMAGE_QUALITY);
+
+      String newPath = imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality);
+      resultMap.put(cache.MAP_KEY_PATH, newPath);
+    }
+    if (resultMap.isEmpty()) {
+      result.success(null);
+    } else {
+      result.success(resultMap);
+    }
+    cache.clear();
+  }
+
+  public void chooseVideoFromGallery(MethodCall methodCall, MethodChannel.Result result) {
+    if (!setPendingMethodCallAndResult(methodCall, result)) {
+      finishWithAlreadyActiveError(result);
+      return;
+    }
+
+    if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
+      permissionManager.askForPermission(
+          Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION);
+      return;
+    }
+
+    launchPickVideoFromGalleryIntent();
+  }
+
+  private void launchPickVideoFromGalleryIntent() {
+    Intent pickVideoIntent = new Intent(Intent.ACTION_GET_CONTENT);
+    pickVideoIntent.setType("video/*");
+
+    activity.startActivityForResult(pickVideoIntent, REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY);
+  }
+
+  public void takeVideoWithCamera(MethodCall methodCall, MethodChannel.Result result) {
+    if (!setPendingMethodCallAndResult(methodCall, result)) {
+      finishWithAlreadyActiveError(result);
+      return;
+    }
+
+    if (needRequestCameraPermission()
+        && !permissionManager.isPermissionGranted(Manifest.permission.CAMERA)) {
+      permissionManager.askForPermission(
+          Manifest.permission.CAMERA, REQUEST_CAMERA_VIDEO_PERMISSION);
+      return;
+    }
+
+    launchTakeVideoWithCameraIntent();
+  }
+
+  private void launchTakeVideoWithCameraIntent() {
+    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+    if (this.methodCall != null && this.methodCall.argument("maxDuration") != null) {
+      int maxSeconds = this.methodCall.argument("maxDuration");
+      intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, maxSeconds);
+    }
+    if (cameraDevice == CameraDevice.FRONT) {
+      useFrontCamera(intent);
+    }
+
+    boolean canTakePhotos = intentResolver.resolveActivity(intent);
+
+    if (!canTakePhotos) {
+      finishWithError("no_available_camera", "No cameras available for taking pictures.");
+      return;
+    }
+
+    File videoFile = createTemporaryWritableVideoFile();
+    pendingCameraMediaUri = Uri.parse("file:" + videoFile.getAbsolutePath());
+
+    Uri videoUri = fileUriResolver.resolveFileProviderUriForFile(fileProviderName, videoFile);
+    intent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri);
+    grantUriPermissions(intent, videoUri);
+
+    activity.startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA);
+  }
+
+  public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result result) {
+    if (!setPendingMethodCallAndResult(methodCall, result)) {
+      finishWithAlreadyActiveError(result);
+      return;
+    }
+
+    if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
+      permissionManager.askForPermission(
+          Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION);
+      return;
+    }
+
+    launchPickImageFromGalleryIntent();
+  }
+
+  private void launchPickImageFromGalleryIntent() {
+    Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
+    pickImageIntent.setType("image/*");
+
+    activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
+  }
+
+  public void takeImageWithCamera(MethodCall methodCall, MethodChannel.Result result) {
+    if (!setPendingMethodCallAndResult(methodCall, result)) {
+      finishWithAlreadyActiveError(result);
+      return;
+    }
+
+    if (needRequestCameraPermission()
+        && !permissionManager.isPermissionGranted(Manifest.permission.CAMERA)) {
+      permissionManager.askForPermission(
+          Manifest.permission.CAMERA, REQUEST_CAMERA_IMAGE_PERMISSION);
+      return;
+    }
+    launchTakeImageWithCameraIntent();
+  }
+
+  private boolean needRequestCameraPermission() {
+    if (permissionManager == null) {
+      return false;
+    }
+    return permissionManager.needRequestCameraPermission();
+  }
+
+  private void launchTakeImageWithCameraIntent() {
+    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+    if (cameraDevice == CameraDevice.FRONT) {
+      useFrontCamera(intent);
+    }
+
+    boolean canTakePhotos = intentResolver.resolveActivity(intent);
+
+    if (!canTakePhotos) {
+      finishWithError("no_available_camera", "No cameras available for taking pictures.");
+      return;
+    }
+
+    File imageFile = createTemporaryWritableImageFile();
+    pendingCameraMediaUri = Uri.parse("file:" + imageFile.getAbsolutePath());
+
+    Uri imageUri = fileUriResolver.resolveFileProviderUriForFile(fileProviderName, imageFile);
+    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
+    grantUriPermissions(intent, imageUri);
+
+    activity.startActivityForResult(intent, REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA);
+  }
+
+  private File createTemporaryWritableImageFile() {
+    return createTemporaryWritableFile(".jpg");
+  }
+
+  private File createTemporaryWritableVideoFile() {
+    return createTemporaryWritableFile(".mp4");
+  }
+
+  private File createTemporaryWritableFile(String suffix) {
+    String filename = UUID.randomUUID().toString();
+    File image;
+
+    try {
+      image = File.createTempFile(filename, suffix, externalFilesDirectory);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    return image;
+  }
+
+  private void grantUriPermissions(Intent intent, Uri imageUri) {
+    PackageManager packageManager = activity.getPackageManager();
+    List<ResolveInfo> compatibleActivities =
+        packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+
+    for (ResolveInfo info : compatibleActivities) {
+      activity.grantUriPermission(
+          info.activityInfo.packageName,
+          imageUri,
+          Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+    }
+  }
+
+  @Override
+  public boolean onRequestPermissionsResult(
+      int requestCode, String[] permissions, int[] grantResults) {
+    boolean permissionGranted =
+        grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+
+    switch (requestCode) {
+      case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION:
+        if (permissionGranted) {
+          launchPickImageFromGalleryIntent();
+        }
+        break;
+      case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION:
+        if (permissionGranted) {
+          launchPickVideoFromGalleryIntent();
+        }
+        break;
+      case REQUEST_CAMERA_IMAGE_PERMISSION:
+        if (permissionGranted) {
+          launchTakeImageWithCameraIntent();
+        }
+        break;
+      case REQUEST_CAMERA_VIDEO_PERMISSION:
+        if (permissionGranted) {
+          launchTakeVideoWithCameraIntent();
+        }
+        break;
+      default:
+        return false;
+    }
+
+    if (!permissionGranted) {
+      switch (requestCode) {
+        case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION:
+        case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION:
+          finishWithError("photo_access_denied", "The user did not allow photo access.");
+          break;
+        case REQUEST_CAMERA_IMAGE_PERMISSION:
+        case REQUEST_CAMERA_VIDEO_PERMISSION:
+          finishWithError("camera_access_denied", "The user did not allow camera access.");
+          break;
+      }
+    }
+
+    return true;
+  }
+
+  @Override
+  public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+    switch (requestCode) {
+      case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY:
+        handleChooseImageResult(resultCode, data);
+        break;
+      case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
+        handleCaptureImageResult(resultCode);
+        break;
+      case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY:
+        handleChooseVideoResult(resultCode, data);
+        break;
+      case REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA:
+        handleCaptureVideoResult(resultCode);
+        break;
+      default:
+        return false;
+    }
+
+    return true;
+  }
+
+  private void handleChooseImageResult(int resultCode, Intent data) {
+    if (resultCode == Activity.RESULT_OK && data != null) {
+      String path = fileUtils.getPathFromUri(activity, data.getData(),false);
+      handleImageResult(path, false);
+      return;
+    }
+
+    // User cancelled choosing a picture.
+    finishWithSuccess(null);
+  }
+
+  private void handleChooseVideoResult(int resultCode, Intent data) {
+    if (resultCode == Activity.RESULT_OK && data != null) {
+      String path = fileUtils.getPathFromUri(activity, data.getData(),true);
+      handleVideoResult(path);
+      return;
+    }
+
+    // User cancelled choosing a picture.
+    finishWithSuccess(null);
+  }
+
+  private void handleCaptureImageResult(int resultCode) {
+    if (resultCode == Activity.RESULT_OK) {
+      fileUriResolver.getFullImagePath(
+          pendingCameraMediaUri != null
+              ? pendingCameraMediaUri
+              : Uri.parse(cache.retrievePendingCameraMediaUriPath()),
+          new OnPathReadyListener() {
+            @Override
+            public void onPathReady(String path) {
+              handleImageResult(path, true);
+            }
+          });
+      return;
+    }
+
+    // User cancelled taking a picture.
+    finishWithSuccess(null);
+  }
+
+  private void handleCaptureVideoResult(int resultCode) {
+    if (resultCode == Activity.RESULT_OK) {
+      fileUriResolver.getFullImagePath(
+          pendingCameraMediaUri != null
+              ? pendingCameraMediaUri
+              : Uri.parse(cache.retrievePendingCameraMediaUriPath()),
+          new OnPathReadyListener() {
+            @Override
+            public void onPathReady(String path) {
+              handleVideoResult(path);
+            }
+          });
+      return;
+    }
+
+    // User cancelled taking a picture.
+    finishWithSuccess(null);
+  }
+
+  private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
+    if (methodCall != null) {
+      Double maxWidth = methodCall.argument("maxWidth");
+      Double maxHeight = methodCall.argument("maxHeight");
+      Integer imageQuality = methodCall.argument("imageQuality");
+
+      String finalImagePath =
+          imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality);
+
+      finishWithSuccess(finalImagePath);
+
+      //delete original file if scaled
+      if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) {
+        new File(path).delete();
+      }
+    } else {
+      finishWithSuccess(path);
+    }
+  }
+
+  private void handleVideoResult(String path) {
+    finishWithSuccess(path);
+  }
+
+  private boolean setPendingMethodCallAndResult(
+      MethodCall methodCall, MethodChannel.Result result) {
+    if (pendingResult != null) {
+      return false;
+    }
+
+    this.methodCall = methodCall;
+    pendingResult = result;
+
+    // Clean up cache if a new image picker is launched.
+    cache.clear();
+
+    return true;
+  }
+
+  private void finishWithSuccess(String imagePath) {
+    if (pendingResult == null) {
+      cache.saveResult(imagePath, null, null);
+      return;
+    }
+    pendingResult.success(imagePath);
+    clearMethodCallAndResult();
+  }
+
+  private void finishWithAlreadyActiveError(MethodChannel.Result result) {
+    result.error("already_active", "Image picker is already active", null);
+  }
+
+  private void finishWithError(String errorCode, String errorMessage) {
+    if (pendingResult == null) {
+      cache.saveResult(null, errorCode, errorMessage);
+      return;
+    }
+    pendingResult.error(errorCode, errorMessage, null);
+    clearMethodCallAndResult();
+  }
+
+  private void clearMethodCallAndResult() {
+    methodCall = null;
+    pendingResult = null;
+  }
+
+  private void useFrontCamera(Intent intent) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+      intent.putExtra(
+          "android.intent.extras.CAMERA_FACING", CameraCharacteristics.LENS_FACING_FRONT);
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        intent.putExtra("android.intent.extra.USE_FRONT_CAMERA", true);
+      }
+    } else {
+      intent.putExtra("android.intent.extras.CAMERA_FACING", 1);
+    }
+  }
+}

+ 14 - 0
android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java

@@ -0,0 +1,14 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import androidx.core.content.FileProvider;
+
+/**
+ * Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
+ *
+ * <p>See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
+ */
+public class ImagePickerFileProvider extends FileProvider {}

+ 325 - 0
android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java

@@ -0,0 +1,325 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+import java.io.File;
+
+@SuppressWarnings("deprecation")
+public class ImagePickerPlugin
+    implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware {
+
+  private class LifeCycleObserver
+      implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {
+    private final Activity thisActivity;
+
+    LifeCycleObserver(Activity activity) {
+      this.thisActivity = activity;
+    }
+
+    @Override
+    public void onCreate(@NonNull LifecycleOwner owner) {}
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {}
+
+    @Override
+    public void onResume(@NonNull LifecycleOwner owner) {}
+
+    @Override
+    public void onPause(@NonNull LifecycleOwner owner) {}
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+      onActivityStopped(thisActivity);
+    }
+
+    @Override
+    public void onDestroy(@NonNull LifecycleOwner owner) {
+      onActivityDestroyed(thisActivity);
+    }
+
+    @Override
+    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
+
+    @Override
+    public void onActivityStarted(Activity activity) {}
+
+    @Override
+    public void onActivityResumed(Activity activity) {}
+
+    @Override
+    public void onActivityPaused(Activity activity) {}
+
+    @Override
+    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
+
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+      if (thisActivity == activity && activity.getApplicationContext() != null) {
+        ((Application) activity.getApplicationContext())
+            .unregisterActivityLifecycleCallbacks(
+                this); // Use getApplicationContext() to avoid casting failures
+      }
+    }
+
+    @Override
+    public void onActivityStopped(Activity activity) {
+      if (thisActivity == activity) {
+        delegate.saveStateBeforeResult();
+      }
+    }
+  }
+
+  static final String METHOD_CALL_IMAGE = "pickImage";
+  static final String METHOD_CALL_VIDEO = "pickVideo";
+  private static final String METHOD_CALL_RETRIEVE = "retrieve";
+  private static final int CAMERA_DEVICE_FRONT = 1;
+  private static final int CAMERA_DEVICE_REAR = 0;
+  private static final String CHANNEL = "plugins.flutter.io/image_picker";
+
+  private static final int SOURCE_CAMERA = 0;
+  private static final int SOURCE_GALLERY = 1;
+
+  private MethodChannel channel;
+  private ImagePickerDelegate delegate;
+  private FlutterPluginBinding pluginBinding;
+  private ActivityPluginBinding activityBinding;
+  private Application application;
+  private Activity activity;
+  // This is null when not using v2 embedding;
+  private Lifecycle lifecycle;
+  private LifeCycleObserver observer;
+
+  public static void registerWith(PluginRegistry.Registrar registrar) {
+    if (registrar.activity() == null) {
+      // If a background flutter view tries to register the plugin, there will be no activity from the registrar,
+      // we stop the registering process immediately because the ImagePicker requires an activity.
+      return;
+    }
+    Activity activity = registrar.activity();
+    Application application = null;
+    if (registrar.context() != null) {
+      application = (Application) (registrar.context().getApplicationContext());
+    }
+    ImagePickerPlugin plugin = new ImagePickerPlugin();
+    plugin.setup(registrar.messenger(), application, activity, registrar, null);
+  }
+
+  /**
+   * Default constructor for the plugin.
+   *
+   * <p>Use this constructor for production code.
+   */
+  // See also: * {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing.
+  public ImagePickerPlugin() {}
+
+  @VisibleForTesting
+  ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) {
+    this.delegate = delegate;
+    this.activity = activity;
+  }
+
+  @Override
+  public void onAttachedToEngine(FlutterPluginBinding binding) {
+    pluginBinding = binding;
+  }
+
+  @Override
+  public void onDetachedFromEngine(FlutterPluginBinding binding) {
+    pluginBinding = null;
+  }
+
+  @Override
+  public void onAttachedToActivity(ActivityPluginBinding binding) {
+    activityBinding = binding;
+    setup(
+        pluginBinding.getBinaryMessenger(),
+        (Application) pluginBinding.getApplicationContext(),
+        activityBinding.getActivity(),
+        null,
+        activityBinding);
+  }
+
+  @Override
+  public void onDetachedFromActivity() {
+    tearDown();
+  }
+
+  @Override
+  public void onDetachedFromActivityForConfigChanges() {
+    onDetachedFromActivity();
+  }
+
+  @Override
+  public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
+    onAttachedToActivity(binding);
+  }
+
+  private void setup(
+      final BinaryMessenger messenger,
+      final Application application,
+      final Activity activity,
+      final PluginRegistry.Registrar registrar,
+      final ActivityPluginBinding activityBinding) {
+    this.activity = activity;
+    this.application = application;
+    this.delegate = constructDelegate(activity);
+    channel = new MethodChannel(messenger, CHANNEL);
+    channel.setMethodCallHandler(this);
+    observer = new LifeCycleObserver(activity);
+    if (registrar != null) {
+      // V1 embedding setup for activity listeners.
+      application.registerActivityLifecycleCallbacks(observer);
+      registrar.addActivityResultListener(delegate);
+      registrar.addRequestPermissionsResultListener(delegate);
+    } else {
+      // V2 embedding setup for activity listeners.
+      activityBinding.addActivityResultListener(delegate);
+      activityBinding.addRequestPermissionsResultListener(delegate);
+      lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
+      lifecycle.addObserver(observer);
+    }
+  }
+
+  private void tearDown() {
+    activityBinding.removeActivityResultListener(delegate);
+    activityBinding.removeRequestPermissionsResultListener(delegate);
+    activityBinding = null;
+    lifecycle.removeObserver(observer);
+    lifecycle = null;
+    delegate = null;
+    channel.setMethodCallHandler(null);
+    channel = null;
+    application.unregisterActivityLifecycleCallbacks(observer);
+    application = null;
+  }
+
+  private final ImagePickerDelegate constructDelegate(final Activity setupActivity) {
+    final ImagePickerCache cache = new ImagePickerCache(setupActivity);
+
+    final File externalFilesDirectory =
+        setupActivity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+    final ExifDataCopier exifDataCopier = new ExifDataCopier();
+    final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier);
+    return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache);
+  }
+
+  // MethodChannel.Result wrapper that responds on the platform thread.
+  private static class MethodResultWrapper implements MethodChannel.Result {
+    private MethodChannel.Result methodResult;
+    private Handler handler;
+
+    MethodResultWrapper(MethodChannel.Result result) {
+      methodResult = result;
+      handler = new Handler(Looper.getMainLooper());
+    }
+
+    @Override
+    public void success(final Object result) {
+      handler.post(
+          new Runnable() {
+            @Override
+            public void run() {
+              methodResult.success(result);
+            }
+          });
+    }
+
+    @Override
+    public void error(
+        final String errorCode, final String errorMessage, final Object errorDetails) {
+      handler.post(
+          new Runnable() {
+            @Override
+            public void run() {
+              methodResult.error(errorCode, errorMessage, errorDetails);
+            }
+          });
+    }
+
+    @Override
+    public void notImplemented() {
+      handler.post(
+          new Runnable() {
+            @Override
+            public void run() {
+              methodResult.notImplemented();
+            }
+          });
+    }
+  }
+
+  @Override
+  public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) {
+    if (activity == null) {
+      rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null);
+      return;
+    }
+    MethodChannel.Result result = new MethodResultWrapper(rawResult);
+    int imageSource;
+    if (call.argument("cameraDevice") != null) {
+      CameraDevice device;
+      int deviceIntValue = call.argument("cameraDevice");
+      if (deviceIntValue == CAMERA_DEVICE_FRONT) {
+        device = CameraDevice.FRONT;
+      } else {
+        device = CameraDevice.REAR;
+      }
+      delegate.setCameraDevice(device);
+    }
+    switch (call.method) {
+      case METHOD_CALL_IMAGE:
+        imageSource = call.argument("source");
+        switch (imageSource) {
+          case SOURCE_GALLERY:
+            delegate.chooseImageFromGallery(call, result);
+            break;
+          case SOURCE_CAMERA:
+            delegate.takeImageWithCamera(call, result);
+            break;
+          default:
+            throw new IllegalArgumentException("Invalid image source: " + imageSource);
+        }
+        break;
+      case METHOD_CALL_VIDEO:
+        imageSource = call.argument("source");
+        switch (imageSource) {
+          case SOURCE_GALLERY:
+            delegate.chooseVideoFromGallery(call, result);
+            break;
+          case SOURCE_CAMERA:
+            delegate.takeVideoWithCamera(call, result);
+            break;
+          default:
+            throw new IllegalArgumentException("Invalid video source: " + imageSource);
+        }
+        break;
+      case METHOD_CALL_RETRIEVE:
+        delegate.retrieveLostImage(result);
+        break;
+      default:
+        throw new IllegalArgumentException("Unknown method " + call.method);
+    }
+  }
+}

+ 43 - 0
android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java

@@ -0,0 +1,43 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import java.util.Arrays;
+
+final class ImagePickerUtils {
+  /** returns true, if permission present in manifest, otherwise false */
+  private static boolean isPermissionPresentInManifest(Context context, String permissionName) {
+    try {
+      PackageManager packageManager = context.getPackageManager();
+      PackageInfo packageInfo =
+          packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
+
+      String[] requestedPermissions = packageInfo.requestedPermissions;
+      return Arrays.asList(requestedPermissions).contains(permissionName);
+    } catch (PackageManager.NameNotFoundException e) {
+      e.printStackTrace();
+      return false;
+    }
+  }
+
+  /**
+   * Camera permission need request if it present in manifest, because for M or great for take Photo
+   * ar Video by intent need it permission, even if the camera permission is not used.
+   *
+   * <p>Camera permission may be used in another package, as example flutter_barcode_reader.
+   * https://github.com/flutter/flutter/issues/29837
+   *
+   * @return returns true, if need request camera permission, otherwise false
+   */
+  static boolean needRequestCameraPermission(Context context) {
+    boolean greatOrEqualM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+    return greatOrEqualM && isPermissionPresentInManifest(context, Manifest.permission.CAMERA);
+  }
+}

+ 154 - 0
android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java

@@ -0,0 +1,154 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+class ImageResizer {
+  private final File externalFilesDirectory;
+  private final ExifDataCopier exifDataCopier;
+
+  ImageResizer(File externalFilesDirectory, ExifDataCopier exifDataCopier) {
+    this.externalFilesDirectory = externalFilesDirectory;
+    this.exifDataCopier = exifDataCopier;
+  }
+
+  /**
+   * If necessary, resizes the image located in imagePath and then returns the path for the scaled
+   * image.
+   *
+   * <p>If no resizing is needed, returns the path for the original image.
+   */
+  String resizeImageIfNeeded(
+      String imagePath,
+      @Nullable Double maxWidth,
+      @Nullable Double maxHeight,
+      @Nullable Integer imageQuality) {
+    Bitmap bmp = decodeFile(imagePath);
+    if (bmp == null) {
+      return null;
+    }
+    boolean shouldScale =
+        maxWidth != null || maxHeight != null || isImageQualityValid(imageQuality);
+    if (!shouldScale) {
+      return imagePath;
+    }
+    try {
+      String[] pathParts = imagePath.split("/");
+      String imageName = pathParts[pathParts.length - 1];
+      File file = resizedImage(bmp, maxWidth, maxHeight, imageQuality, imageName);
+      copyExif(imagePath, file.getPath());
+      return file.getPath();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private File resizedImage(
+      Bitmap bmp, Double maxWidth, Double maxHeight, Integer imageQuality, String outputImageName)
+      throws IOException {
+    double originalWidth = bmp.getWidth() * 1.0;
+    double originalHeight = bmp.getHeight() * 1.0;
+
+    if (!isImageQualityValid(imageQuality)) {
+      imageQuality = 100;
+    }
+
+    boolean hasMaxWidth = maxWidth != null;
+    boolean hasMaxHeight = maxHeight != null;
+
+    Double width = hasMaxWidth ? Math.min(originalWidth, maxWidth) : originalWidth;
+    Double height = hasMaxHeight ? Math.min(originalHeight, maxHeight) : originalHeight;
+
+    boolean shouldDownscaleWidth = hasMaxWidth && maxWidth < originalWidth;
+    boolean shouldDownscaleHeight = hasMaxHeight && maxHeight < originalHeight;
+    boolean shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight;
+
+    if (shouldDownscale) {
+      double downscaledWidth = (height / originalHeight) * originalWidth;
+      double downscaledHeight = (width / originalWidth) * originalHeight;
+
+      if (width < height) {
+        if (!hasMaxWidth) {
+          width = downscaledWidth;
+        } else {
+          height = downscaledHeight;
+        }
+      } else if (height < width) {
+        if (!hasMaxHeight) {
+          height = downscaledHeight;
+        } else {
+          width = downscaledWidth;
+        }
+      } else {
+        if (originalWidth < originalHeight) {
+          width = downscaledWidth;
+        } else if (originalHeight < originalWidth) {
+          height = downscaledHeight;
+        }
+      }
+    }
+
+    Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false);
+    File file =
+        createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality);
+    return file;
+  }
+
+  private File createFile(File externalFilesDirectory, String child) {
+    File image = new File(externalFilesDirectory, child);
+    if (!image.getParentFile().exists()) {
+      image.getParentFile().mkdirs();
+    }
+    return image;
+  }
+
+  private FileOutputStream createOutputStream(File imageFile) throws IOException {
+    return new FileOutputStream(imageFile);
+  }
+
+  private void copyExif(String filePathOri, String filePathDest) {
+    exifDataCopier.copyExif(filePathOri, filePathDest);
+  }
+
+  private Bitmap decodeFile(String path) {
+    return BitmapFactory.decodeFile(path);
+  }
+
+  private Bitmap createScaledBitmap(Bitmap bmp, int width, int height, boolean filter) {
+    return Bitmap.createScaledBitmap(bmp, width, height, filter);
+  }
+
+  private boolean isImageQualityValid(Integer imageQuality) {
+    return imageQuality != null && imageQuality > 0 && imageQuality < 100;
+  }
+
+  private File createImageOnExternalDirectory(String name, Bitmap bitmap, int imageQuality)
+      throws IOException {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    boolean saveAsPNG = bitmap.hasAlpha();
+    if (saveAsPNG) {
+      Log.d(
+          "ImageResizer",
+          "image_picker: compressing is not supported for type PNG. Returning the image with original quality");
+    }
+    bitmap.compress(
+        saveAsPNG ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG,
+        imageQuality,
+        outputStream);
+    File imageFile = createFile(externalFilesDirectory, name);
+    FileOutputStream fileOutput = createOutputStream(imageFile);
+    fileOutput.write(outputStream.toByteArray());
+    fileOutput.close();
+    return imageFile;
+  }
+}

+ 4 - 0
android/src/main/res/xml/flutter_image_picker_file_paths.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+    <external-path name="external_files" path="."/>
+</paths>

+ 8 - 0
example/README.md

@@ -0,0 +1,8 @@
+# image_picker_example
+
+Demonstrates how to use the image_picker plugin.
+
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](http://flutter.io/).

+ 12 - 0
example/android.iml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$/android">
+      <sourceFolder url="file://$MODULE_DIR$/android/app/src/main/java" isTestSource="false" />
+    </content>
+    <orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Flutter for Android" level="project" />
+  </component>
+</module>

+ 68 - 0
example/android/app/build.gradle

@@ -0,0 +1,68 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 28
+    testOptions.unitTests.includeAndroidResources = true
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        applicationId "io.flutter.plugins.imagepicker.example"
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig signingConfigs.debug
+        }
+    }
+
+    testOptions {
+        unitTests.returnDefaultValues = true
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    testImplementation 'junit:junit:4.12'
+    testImplementation 'org.mockito:mockito-core:2.17.0'
+    androidTestImplementation 'androidx.test:runner:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+    testImplementation 'androidx.test:core:1.2.0'
+    testImplementation "org.robolectric:robolectric:4.3.1"
+}

+ 5 - 0
example/android/app/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 26 - 0
example/android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,26 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.flutter.plugins.imagepickerexample">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <application android:label="Image Picker Example" android:icon="@mipmap/ic_launcher">
+        <activity android:name="io.flutter.embedding.android.FlutterActivity"
+                  android:theme="@android:style/Theme.Black.NoTitleBar"
+                  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+                  android:hardwareAccelerated="true"
+                  android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".EmbeddingV1Activity"
+                  android:theme="@android:style/Theme.Black.NoTitleBar"
+                  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+                  android:hardwareAccelerated="true"
+                  android:windowSoftInputMode="adjustResize">
+        </activity>
+        <meta-data android:name="flutterEmbedding" android:value="2"/>
+    </application>
+</manifest>

+ 1 - 0
example/android/app/src/main/java/io/flutter/plugins/.gitignore

@@ -0,0 +1 @@
+GeneratedPluginRegistrant.java

+ 21 - 0
example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java

@@ -0,0 +1,21 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepickerexample;
+
+import android.os.Bundle;
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugins.imagepicker.ImagePickerPlugin;
+import io.flutter.plugins.videoplayer.VideoPlayerPlugin;
+
+public class EmbeddingV1Activity extends FlutterActivity {
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    ImagePickerPlugin.registerWith(
+        registrarFor("io.flutter.plugins.imagepicker.ImagePickerPlugin"));
+    VideoPlayerPlugin.registerWith(
+        registrarFor("io.flutter.plugins.videoplayer.VideoPlayerPlugin"));
+  }
+}

+ 17 - 0
example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java

@@ -0,0 +1,17 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepickerexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.e2e.FlutterRunner;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterRunner.class)
+public class EmbeddingV1ActivityTest {
+  @Rule
+  public ActivityTestRule<EmbeddingV1Activity> rule =
+      new ActivityTestRule<>(EmbeddingV1Activity.class);
+}

+ 13 - 0
example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java

@@ -0,0 +1,13 @@
+package io.flutter.plugins.imagepickerexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.e2e.FlutterRunner;
+import io.flutter.embedding.android.FlutterActivity;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterRunner.class)
+public class FlutterActivityTest {
+  @Rule
+  public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
+}

BIN
example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


+ 57 - 0
example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java

@@ -0,0 +1,57 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.net.Uri;
+import androidx.test.core.app.ApplicationProvider;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowContentResolver;
+
+@RunWith(RobolectricTestRunner.class)
+public class FileUtilTest {
+
+  private Context context;
+  private FileUtils fileUtils;
+  ShadowContentResolver shadowContentResolver;
+
+  @Before
+  public void before() {
+    context = ApplicationProvider.getApplicationContext();
+    shadowContentResolver = shadowOf(context.getContentResolver());
+    fileUtils = new FileUtils();
+  }
+
+  @Test
+  public void FileUtil_GetPathFromUri() throws IOException {
+    Uri uri = Uri.parse("content://dummy/dummy.png");
+    shadowContentResolver.registerInputStream(
+        uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8)));
+    String path = fileUtils.getPathFromUri(context, uri);
+    File file = new File(path);
+    int size = (int) file.length();
+    byte[] bytes = new byte[size];
+
+    BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file));
+    buf.read(bytes, 0, bytes.length);
+    buf.close();
+
+    assertTrue(bytes.length > 0);
+    String imageStream = new String(bytes, UTF_8);
+    assertTrue(imageStream.equals("imageStream"));
+  }
+}

+ 115 - 0
example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java

@@ -0,0 +1,115 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import static io.flutter.plugins.imagepicker.ImagePickerCache.MAP_KEY_IMAGE_QUALITY;
+import static io.flutter.plugins.imagepicker.ImagePickerCache.SHARED_PREFERENCES_NAME;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import io.flutter.plugin.common.MethodCall;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ImagePickerCacheTest {
+  private static final int IMAGE_QUALITY = 90;
+
+  @Mock Activity mockActivity;
+  @Mock SharedPreferences mockPreference;
+  @Mock SharedPreferences.Editor mockEditor;
+  @Mock MethodCall mockMethodCall;
+
+  static Map<String, Object> preferenceStorage;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+
+    preferenceStorage = new HashMap();
+    when(mockActivity.getPackageName()).thenReturn("com.example.test");
+    when(mockActivity.getPackageManager()).thenReturn(mock(PackageManager.class));
+    when(mockActivity.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE))
+        .thenReturn(mockPreference);
+    when(mockPreference.edit()).thenReturn(mockEditor);
+    when(mockEditor.putInt(any(String.class), any(int.class)))
+        .then(
+            i -> {
+              preferenceStorage.put(i.getArgument(0), i.getArgument(1));
+              return mockEditor;
+            });
+    when(mockEditor.putLong(any(String.class), any(long.class)))
+        .then(
+            i -> {
+              preferenceStorage.put(i.getArgument(0), i.getArgument(1));
+              return mockEditor;
+            });
+    when(mockEditor.putString(any(String.class), any(String.class)))
+        .then(
+            i -> {
+              preferenceStorage.put(i.getArgument(0), i.getArgument(1));
+              return mockEditor;
+            });
+
+    when(mockPreference.getInt(any(String.class), any(int.class)))
+        .then(
+            i -> {
+              int result =
+                  (int)
+                      ((preferenceStorage.get(i.getArgument(0)) != null)
+                          ? preferenceStorage.get(i.getArgument(0))
+                          : i.getArgument(1));
+              return result;
+            });
+    when(mockPreference.getLong(any(String.class), any(long.class)))
+        .then(
+            i -> {
+              long result =
+                  (long)
+                      ((preferenceStorage.get(i.getArgument(0)) != null)
+                          ? preferenceStorage.get(i.getArgument(0))
+                          : i.getArgument(1));
+              return result;
+            });
+    when(mockPreference.getString(any(String.class), any(String.class)))
+        .then(
+            i -> {
+              String result =
+                  (String)
+                      ((preferenceStorage.get(i.getArgument(0)) != null)
+                          ? preferenceStorage.get(i.getArgument(0))
+                          : i.getArgument(1));
+              return result;
+            });
+
+    when(mockPreference.contains(any(String.class))).thenReturn(true);
+  }
+
+  @Test
+  public void ImageCache_ShouldBeAbleToSetAndGetQuality() {
+    when(mockMethodCall.argument(MAP_KEY_IMAGE_QUALITY)).thenReturn(IMAGE_QUALITY);
+    ImagePickerCache cache = new ImagePickerCache(mockActivity);
+    cache.saveDimensionWithMethodCall(mockMethodCall);
+    Map<String, Object> resultMap = cache.getCacheMap();
+    int imageQuality = (int) resultMap.get(cache.MAP_KEY_IMAGE_QUALITY);
+    assertThat(imageQuality, equalTo(IMAGE_QUALITY));
+
+    when(mockMethodCall.argument(MAP_KEY_IMAGE_QUALITY)).thenReturn(null);
+    cache.saveDimensionWithMethodCall(mockMethodCall);
+    Map<String, Object> resultMapWithDefaultQuality = cache.getCacheMap();
+    int defaultImageQuality = (int) resultMapWithDefaultQuality.get(cache.MAP_KEY_IMAGE_QUALITY);
+    assertThat(defaultImageQuality, equalTo(100));
+  }
+}

+ 421 - 0
example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java

@@ -0,0 +1,421 @@
+package io.flutter.plugins.imagepicker;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import java.io.File;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ImagePickerDelegateTest {
+  private static final Double WIDTH = 10.0;
+  private static final Double HEIGHT = 10.0;
+  private static final Double MAX_DURATION = 10.0;
+  private static final Integer IMAGE_QUALITY = 90;
+
+  @Mock Activity mockActivity;
+  @Mock ImageResizer mockImageResizer;
+  @Mock MethodCall mockMethodCall;
+  @Mock MethodChannel.Result mockResult;
+  @Mock ImagePickerDelegate.PermissionManager mockPermissionManager;
+  @Mock ImagePickerDelegate.IntentResolver mockIntentResolver;
+  @Mock FileUtils mockFileUtils;
+  @Mock Intent mockIntent;
+  @Mock ImagePickerCache cache;
+
+  ImagePickerDelegate.FileUriResolver mockFileUriResolver;
+
+  private static class MockFileUriResolver implements ImagePickerDelegate.FileUriResolver {
+    @Override
+    public Uri resolveFileProviderUriForFile(String fileProviderName, File imageFile) {
+      return null;
+    }
+
+    @Override
+    public void getFullImagePath(Uri imageUri, ImagePickerDelegate.OnPathReadyListener listener) {
+      listener.onPathReady("pathFromUri");
+    }
+  }
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+
+    when(mockActivity.getPackageName()).thenReturn("com.example.test");
+    when(mockActivity.getPackageManager()).thenReturn(mock(PackageManager.class));
+
+    when(mockFileUtils.getPathFromUri(any(Context.class), any(Uri.class)))
+        .thenReturn("pathFromUri");
+
+    when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null, null))
+        .thenReturn("originalPath");
+    when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null, IMAGE_QUALITY))
+        .thenReturn("originalPath");
+    when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, HEIGHT, null))
+        .thenReturn("scaledPath");
+    when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, null, null))
+        .thenReturn("scaledPath");
+    when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, HEIGHT, null))
+        .thenReturn("scaledPath");
+
+    mockFileUriResolver = new MockFileUriResolver();
+
+    Uri mockUri = mock(Uri.class);
+    when(mockIntent.getData()).thenReturn(mockUri);
+  }
+
+  @Test
+  public void whenConstructed_setsCorrectFileProviderName() {
+    ImagePickerDelegate delegate = createDelegate();
+    assertThat(delegate.fileProviderName, equalTo("com.example.test.flutter.image_provider"));
+  }
+
+  @Test
+  public void chooseImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyActiveError() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.chooseImageFromGallery(mockMethodCall, mockResult);
+
+    verifyFinishedWithAlreadyActiveError();
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void chooseImageFromGallery_WhenHasNoExternalStoragePermission_RequestsForPermission() {
+    when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
+        .thenReturn(false);
+
+    ImagePickerDelegate delegate = createDelegate();
+    delegate.chooseImageFromGallery(mockMethodCall, mockResult);
+
+    verify(mockPermissionManager)
+        .askForPermission(
+            Manifest.permission.READ_EXTERNAL_STORAGE,
+            ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION);
+  }
+
+  @Test
+  public void
+      chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
+    when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
+        .thenReturn(true);
+
+    ImagePickerDelegate delegate = createDelegate();
+    delegate.chooseImageFromGallery(mockMethodCall, mockResult);
+
+    verify(mockActivity)
+        .startActivityForResult(
+            any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY));
+  }
+
+  @Test
+  public void takeImageWithCamera_WhenPendingResultExists_FinishesWithAlreadyActiveError() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.takeImageWithCamera(mockMethodCall, mockResult);
+
+    verifyFinishedWithAlreadyActiveError();
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void takeImageWithCamera_WhenHasNoCameraPermission_RequestsForPermission() {
+    when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(false);
+    when(mockPermissionManager.needRequestCameraPermission()).thenReturn(true);
+
+    ImagePickerDelegate delegate = createDelegate();
+    delegate.takeImageWithCamera(mockMethodCall, mockResult);
+
+    verify(mockPermissionManager)
+        .askForPermission(
+            Manifest.permission.CAMERA, ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION);
+  }
+
+  @Test
+  public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermission() {
+    when(mockPermissionManager.needRequestCameraPermission()).thenReturn(false);
+    when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
+
+    ImagePickerDelegate delegate = createDelegate();
+    delegate.takeImageWithCamera(mockMethodCall, mockResult);
+
+    verify(mockActivity)
+        .startActivityForResult(
+            any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA));
+  }
+
+  @Test
+  public void
+      takeImageWithCamera_WhenHasCameraPermission_AndAnActivityCanHandleCameraIntent_LaunchesTakeWithCameraIntent() {
+    when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true);
+    when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
+
+    ImagePickerDelegate delegate = createDelegate();
+    delegate.takeImageWithCamera(mockMethodCall, mockResult);
+
+    verify(mockActivity)
+        .startActivityForResult(
+            any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA));
+  }
+
+  @Test
+  public void
+      takeImageWithCamera_WhenHasCameraPermission_AndNoActivityToHandleCameraIntent_FinishesWithNoCamerasAvailableError() {
+    when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true);
+    when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(false);
+
+    ImagePickerDelegate delegate = createDelegate();
+    delegate.takeImageWithCamera(mockMethodCall, mockResult);
+
+    verify(mockResult)
+        .error("no_available_camera", "No cameras available for taking pictures.", null);
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onRequestPermissionsResult_WhenReadExternalStoragePermissionDenied_FinishesWithError() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onRequestPermissionsResult(
+        ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION,
+        new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+        new int[] {PackageManager.PERMISSION_DENIED});
+
+    verify(mockResult).error("photo_access_denied", "The user did not allow photo access.", null);
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onRequestChooseImagePermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseImageFromGalleryIntent() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onRequestPermissionsResult(
+        ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION,
+        new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+        new int[] {PackageManager.PERMISSION_GRANTED});
+
+    verify(mockActivity)
+        .startActivityForResult(
+            any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY));
+  }
+
+  @Test
+  public void
+      onRequestChooseVideoPermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseVideoFromGalleryIntent() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onRequestPermissionsResult(
+        ImagePickerDelegate.REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION,
+        new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+        new int[] {PackageManager.PERMISSION_GRANTED});
+
+    verify(mockActivity)
+        .startActivityForResult(
+            any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY));
+  }
+
+  @Test
+  public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithError() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onRequestPermissionsResult(
+        ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION,
+        new String[] {Manifest.permission.CAMERA},
+        new int[] {PackageManager.PERMISSION_DENIED});
+
+    verify(mockResult).error("camera_access_denied", "The user did not allow camera access.", null);
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onRequestTakeVideoPermissionsResult_WhenCameraPermissionGranted_LaunchesTakeVideoWithCameraIntent() {
+    when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
+
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+    delegate.onRequestPermissionsResult(
+        ImagePickerDelegate.REQUEST_CAMERA_VIDEO_PERMISSION,
+        new String[] {Manifest.permission.CAMERA},
+        new int[] {PackageManager.PERMISSION_GRANTED});
+
+    verify(mockActivity)
+        .startActivityForResult(
+            any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA));
+  }
+
+  @Test
+  public void
+      onRequestTakeImagePermissionsResult_WhenCameraPermissionGranted_LaunchesTakeWithCameraIntent() {
+    when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
+
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+    delegate.onRequestPermissionsResult(
+        ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION,
+        new String[] {Manifest.permission.CAMERA},
+        new int[] {PackageManager.PERMISSION_GRANTED});
+
+    verify(mockActivity)
+        .startActivityForResult(
+            any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA));
+  }
+
+  @Test
+  public void onActivityResult_WhenPickFromGalleryCanceled_FinishesWithNull() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_CANCELED, null);
+
+    verify(mockResult).success(null);
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onActivityResult_WhenImagePickedFromGallery_AndNoResizeNeeded_FinishesWithImagePath() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent);
+
+    verify(mockResult).success("originalPath");
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onActivityResult_WhenImagePickedFromGallery_AndResizeNeeded_FinishesWithScaledImagePath() {
+    when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH);
+
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent);
+
+    verify(mockResult).success("scaledPath");
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onActivityResult_WhenVideoPickedFromGallery_AndResizeParametersSupplied_FinishesWithFilePath() {
+    when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH);
+
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, Activity.RESULT_OK, mockIntent);
+
+    verify(mockResult).success("pathFromUri");
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void onActivityResult_WhenTakeImageWithCameraCanceled_FinishesWithNull() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_CANCELED, null);
+
+    verify(mockResult).success(null);
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_FinishesWithImagePath() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
+
+    verify(mockResult).success("originalPath");
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onActivityResult_WhenImageTakenWithCamera_AndResizeNeeded_FinishesWithScaledImagePath() {
+    when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH);
+
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
+
+    verify(mockResult).success("scaledPath");
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onActivityResult_WhenVideoTakenWithCamera_AndResizeParametersSupplied_FinishesWithFilePath() {
+    when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH);
+
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
+
+    verify(mockResult).success("pathFromUri");
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  @Test
+  public void
+      onActivityResult_WhenVideoTakenWithCamera_AndMaxDurationParametersSupplied_FinishesWithFilePath() {
+    when(mockMethodCall.argument("maxDuration")).thenReturn(MAX_DURATION);
+
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+    delegate.onActivityResult(
+        ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
+
+    verify(mockResult).success("pathFromUri");
+    verifyNoMoreInteractions(mockResult);
+  }
+
+  private ImagePickerDelegate createDelegate() {
+    return new ImagePickerDelegate(
+        mockActivity,
+        null,
+        mockImageResizer,
+        null,
+        null,
+        cache,
+        mockPermissionManager,
+        mockIntentResolver,
+        mockFileUriResolver,
+        mockFileUtils);
+  }
+
+  private ImagePickerDelegate createDelegateWithPendingResultAndMethodCall() {
+    return new ImagePickerDelegate(
+        mockActivity,
+        null,
+        mockImageResizer,
+        mockResult,
+        mockMethodCall,
+        cache,
+        mockPermissionManager,
+        mockIntentResolver,
+        mockFileUriResolver,
+        mockFileUtils);
+  }
+
+  private void verifyFinishedWithAlreadyActiveError() {
+    verify(mockResult).error("already_active", "Image picker is already active", null);
+  }
+}

+ 152 - 0
example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java

@@ -0,0 +1,152 @@
+package io.flutter.plugins.imagepicker;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.Application;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ImagePickerPluginTest {
+  private static final int SOURCE_CAMERA = 0;
+  private static final int SOURCE_GALLERY = 1;
+  private static final String PICK_IMAGE = "pickImage";
+  private static final String PICK_VIDEO = "pickVideo";
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+
+  @Mock PluginRegistry.Registrar mockRegistrar;
+  @Mock Activity mockActivity;
+  @Mock Application mockApplication;
+  @Mock ImagePickerDelegate mockImagePickerDelegate;
+  @Mock MethodChannel.Result mockResult;
+
+  ImagePickerPlugin plugin;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    when(mockRegistrar.context()).thenReturn(mockApplication);
+
+    plugin = new ImagePickerPlugin(mockImagePickerDelegate, mockActivity);
+  }
+
+  @Test
+  public void onMethodCall_WhenActivityIsNull_FinishesWithForegroundActivityRequiredError() {
+    MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_GALLERY);
+    ImagePickerPlugin imagePickerPluginWithNullActivity =
+        new ImagePickerPlugin(mockImagePickerDelegate, null);
+    imagePickerPluginWithNullActivity.onMethodCall(call, mockResult);
+    verify(mockResult)
+        .error("no_activity", "image_picker plugin requires a foreground activity.", null);
+    verifyZeroInteractions(mockImagePickerDelegate);
+  }
+
+  @Test
+  public void onMethodCall_WhenCalledWithUnknownMethod_ThrowsException() {
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage("Unknown method test");
+    plugin.onMethodCall(new MethodCall("test", null), mockResult);
+    verifyZeroInteractions(mockImagePickerDelegate);
+    verifyZeroInteractions(mockResult);
+  }
+
+  @Test
+  public void onMethodCall_WhenCalledWithUnknownImageSource_ThrowsException() {
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage("Invalid image source: -1");
+    plugin.onMethodCall(buildMethodCall(PICK_IMAGE, -1), mockResult);
+    verifyZeroInteractions(mockImagePickerDelegate);
+    verifyZeroInteractions(mockResult);
+  }
+
+  @Test
+  public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() {
+    MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_GALLERY);
+    plugin.onMethodCall(call, mockResult);
+    verify(mockImagePickerDelegate).chooseImageFromGallery(eq(call), any());
+    verifyZeroInteractions(mockResult);
+  }
+
+  @Test
+  public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() {
+    MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA);
+    plugin.onMethodCall(call, mockResult);
+    verify(mockImagePickerDelegate).takeImageWithCamera(eq(call), any());
+    verifyZeroInteractions(mockResult);
+  }
+
+  @Test
+  public void onMethodCall_PickingImage_WhenSourceIsCamera_InvokesTakeImageWithCamera_RearCamera() {
+    MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA);
+    HashMap<String, Object> arguments = (HashMap<String, Object>) call.arguments;
+    arguments.put("cameraDevice", 0);
+    plugin.onMethodCall(call, mockResult);
+    verify(mockImagePickerDelegate).setCameraDevice(eq(CameraDevice.REAR));
+  }
+
+  @Test
+  public void
+      onMethodCall_PickingImage_WhenSourceIsCamera_InvokesTakeImageWithCamera_FrontCamera() {
+    MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA);
+    HashMap<String, Object> arguments = (HashMap<String, Object>) call.arguments;
+    arguments.put("cameraDevice", 1);
+    plugin.onMethodCall(call, mockResult);
+    verify(mockImagePickerDelegate).setCameraDevice(eq(CameraDevice.FRONT));
+  }
+
+  @Test
+  public void onMethodCall_PickingVideo_WhenSourceIsCamera_InvokesTakeImageWithCamera_RearCamera() {
+    MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA);
+    HashMap<String, Object> arguments = (HashMap<String, Object>) call.arguments;
+    arguments.put("cameraDevice", 0);
+    plugin.onMethodCall(call, mockResult);
+    verify(mockImagePickerDelegate).setCameraDevice(eq(CameraDevice.REAR));
+  }
+
+  @Test
+  public void
+      onMethodCall_PickingVideo_WhenSourceIsCamera_InvokesTakeImageWithCamera_FrontCamera() {
+    MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA);
+    HashMap<String, Object> arguments = (HashMap<String, Object>) call.arguments;
+    arguments.put("cameraDevice", 1);
+    plugin.onMethodCall(call, mockResult);
+    verify(mockImagePickerDelegate).setCameraDevice(eq(CameraDevice.FRONT));
+  }
+
+  @Test
+  public void onResiter_WhenAcitivityIsNull_ShouldNotCrash() {
+    when(mockRegistrar.activity()).thenReturn(null);
+    ImagePickerPlugin.registerWith((mockRegistrar));
+    assertTrue(
+        "No exception thrown when ImagePickerPlugin.registerWith ran with activity = null", true);
+  }
+
+  @Test
+  public void onConstructor_WhenContextTypeIsActivity_ShouldNotCrash() {
+    new ImagePickerPlugin(mockImagePickerDelegate, mockActivity);
+    assertTrue(
+        "No exception thrown when ImagePickerPlugin() ran with context instanceof Activity", true);
+  }
+
+  private MethodCall buildMethodCall(String method, final int source) {
+    final Map<String, Object> arguments = new HashMap<>();
+    arguments.put("source", source);
+
+    return new MethodCall(method, arguments);
+  }
+}

+ 73 - 0
example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java

@@ -0,0 +1,73 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepicker;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import java.io.File;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+// RobolectricTestRunner always creates a default mock bitmap when reading from file. So we cannot actually test the scaling.
+// But we can still test whether the original or scaled file is created.
+@RunWith(RobolectricTestRunner.class)
+public class ImageResizerTest {
+
+  ImageResizer resizer;
+  File imageFile;
+  File externalDirectory;
+  Bitmap originalImageBitmap;
+
+  @Before
+  public void setUp() throws IOException {
+    MockitoAnnotations.initMocks(this);
+    imageFile = new File(getClass().getClassLoader().getResource("pngImage.png").getFile());
+    originalImageBitmap = BitmapFactory.decodeFile(imageFile.getPath());
+    TemporaryFolder temporaryFolder = new TemporaryFolder();
+    temporaryFolder.create();
+    externalDirectory = temporaryFolder.newFolder("image_picker_testing_path");
+    resizer = new ImageResizer(externalDirectory, new ExifDataCopier());
+  }
+
+  @Test
+  public void onResizeImageIfNeeded_WhenQualityIsNull_ShoultNotResize_ReturnTheUnscaledFile() {
+    String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, null);
+    assertThat(outoutFile, equalTo(imageFile.getPath()));
+  }
+
+  @Test
+  public void onResizeImageIfNeeded_WhenQualityIsNotNull_ShoulResize_ReturnResizedFile() {
+    String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, 50);
+    assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png"));
+  }
+
+  @Test
+  public void onResizeImageIfNeeded_WhenWidthIsNotNull_ShoulResize_ReturnResizedFile() {
+    String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, null, null);
+    assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png"));
+  }
+
+  @Test
+  public void onResizeImageIfNeeded_WhenHeightIsNotNull_ShoulResize_ReturnResizedFile() {
+    String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, 50.0, null);
+    assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png"));
+  }
+
+  @Test
+  public void onResizeImageIfNeeded_WhenParentDirectoryDoesNotExists_ShouldNotCrash() {
+    File nonExistentDirectory = new File(externalDirectory, "/nonExistent");
+    ImageResizer invalidResizer = new ImageResizer(nonExistentDirectory, new ExifDataCopier());
+    String outoutFile = invalidResizer.resizeImageIfNeeded(imageFile.getPath(), null, 50.0, null);
+    assertThat(outoutFile, equalTo(nonExistentDirectory.getPath() + "/scaled_pngImage.png"));
+  }
+}

+ 1 - 0
example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

@@ -0,0 +1 @@
+mock-maker-inline

BIN
example/android/app/src/test/resources/pngImage.png


+ 29 - 0
example/android/build.gradle

@@ -0,0 +1,29 @@
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.3.0'
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 5 - 0
example/android/gradle.properties

@@ -0,0 +1,5 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableUnitTestBinaryResources=true

+ 5 - 0
example/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip

+ 15 - 0
example/android/settings.gradle

@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}

+ 16 - 0
example/image_picker_example.iml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="FLUTTER_MODULE_TYPE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.idea" />
+      <excludeFolder url="file://$MODULE_DIR$/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+      <excludeFolder url="file://$MODULE_DIR$/packages" />
+    </content>
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Dart SDK" level="application" />
+    <orderEntry type="library" name="Dart Packages" level="project" />
+    <orderEntry type="library" name="Dart SDK" level="project" />
+  </component>
+</module>

+ 30 - 0
example/ios/Flutter/AppFrameworkInfo.plist

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>CFBundleDevelopmentRegion</key>
+  <string>en</string>
+  <key>CFBundleExecutable</key>
+  <string>App</string>
+  <key>CFBundleIdentifier</key>
+  <string>io.flutter.flutter.app</string>
+  <key>CFBundleInfoDictionaryVersion</key>
+  <string>6.0</string>
+  <key>CFBundleName</key>
+  <string>App</string>
+  <key>CFBundlePackageType</key>
+  <string>FMWK</string>
+  <key>CFBundleShortVersionString</key>
+  <string>1.0</string>
+  <key>CFBundleSignature</key>
+  <string>????</string>
+  <key>CFBundleVersion</key>
+  <string>1.0</string>
+  <key>UIRequiredDeviceCapabilities</key>
+  <array>
+    <string>arm64</string>
+  </array>
+  <key>MinimumOSVersion</key>
+  <string>8.0</string>
+</dict>
+</plist>

+ 2 - 0
example/ios/Flutter/Debug.xcconfig

@@ -0,0 +1,2 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"

+ 2 - 0
example/ios/Flutter/Release.xcconfig

@@ -0,0 +1,2 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"

+ 668 - 0
example/ios/Runner.xcodeproj/project.pbxproj

@@ -0,0 +1,668 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */; };
+		680049262280D736006DD6AB /* MetaDataUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 680049252280D736006DD6AB /* MetaDataUtilTests.m */; };
+		680049272280D79A006DD6AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+		680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; };
+		680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; };
+		68B9AF72243E4B3F00927CE4 /* ImagePickerPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */; };
+		68F4B464228B3AB500C25614 /* PhotoAssetUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */; };
+		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+		9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; };
+		9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; };
+		9FC8F0EE229FB90B00C8D58F /* ImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */; };
+		F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; };
+		F78AF3192342D9D7008449C7 /* ImagePickerTestImages.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		6800491C2280D368006DD6AB /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+			remoteInfo = Runner;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+		5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+		5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+		680049172280D368006DD6AB /* image_picker_exampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = image_picker_exampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		6800491B2280D368006DD6AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		680049252280D736006DD6AB /* MetaDataUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MetaDataUtilTests.m; path = ../../../ios/Tests/MetaDataUtilTests.m; sourceTree = "<group>"; };
+		680049352280F2B8006DD6AB /* pngImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = pngImage.png; sourceTree = "<group>"; };
+		680049362280F2B8006DD6AB /* jpgImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = jpgImage.jpg; sourceTree = "<group>"; };
+		6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ImagePickerPluginTests.m; path = ../../../ios/Tests/ImagePickerPluginTests.m; sourceTree = "<group>"; };
+		68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = PhotoAssetUtilTests.m; path = ../../../ios/Tests/PhotoAssetUtilTests.m; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		9FC8F0E8229FA49E00C8D58F /* gifImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = gifImage.gif; sourceTree = "<group>"; };
+		9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ImageUtilTests.m; path = ../../../ios/Tests/ImageUtilTests.m; sourceTree = "<group>"; };
+		EC32F6993F4529982D9519F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ImagePickerTestImages.h; path = ../../../ios/Tests/ImagePickerTestImages.h; sourceTree = "<group>"; };
+		F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ImagePickerTestImages.m; path = ../../../ios/Tests/ImagePickerTestImages.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		680049142280D368006DD6AB /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		680049182280D368006DD6AB /* image_picker_exampleTests */ = {
+			isa = PBXGroup;
+			children = (
+				6800491B2280D368006DD6AB /* Info.plist */,
+				9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */,
+				680049252280D736006DD6AB /* MetaDataUtilTests.m */,
+				68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */,
+				F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */,
+				F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */,
+				68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */,
+			);
+			path = image_picker_exampleTests;
+			sourceTree = "<group>";
+		};
+		680049282280E33D006DD6AB /* TestImages */ = {
+			isa = PBXGroup;
+			children = (
+				9FC8F0E8229FA49E00C8D58F /* gifImage.gif */,
+				680049362280F2B8006DD6AB /* jpgImage.jpg */,
+				680049352280F2B8006DD6AB /* pngImage.png */,
+			);
+			path = TestImages;
+			sourceTree = "<group>";
+		};
+		840012C8B5EDBCF56B0E4AC1 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */,
+				5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */,
+			);
+			name = Pods;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				680049282280E33D006DD6AB /* TestImages */,
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				680049182280D368006DD6AB /* image_picker_exampleTests */,
+				97C146EF1CF9000F007C117D /* Products */,
+				840012C8B5EDBCF56B0E4AC1 /* Pods */,
+				CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+				680049172280D368006DD6AB /* image_picker_exampleTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */,
+				5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */,
+				7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+				7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				97C146F11CF9000F007C117D /* Supporting Files */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		97C146F11CF9000F007C117D /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				97C146F21CF9000F007C117D /* main.m */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				EC32F6993F4529982D9519F1 /* libPods-Runner.a */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		680049162280D368006DD6AB /* image_picker_exampleTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 6800491E2280D368006DD6AB /* Build configuration list for PBXNativeTarget "image_picker_exampleTests" */;
+			buildPhases = (
+				680049132280D368006DD6AB /* Sources */,
+				680049142280D368006DD6AB /* Frameworks */,
+				680049152280D368006DD6AB /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				6800491D2280D368006DD6AB /* PBXTargetDependency */,
+			);
+			name = image_picker_exampleTests;
+			productName = image_picker_exampleTests;
+			productReference = 680049172280D368006DD6AB /* image_picker_exampleTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		97C146E61CF9000F007C117D /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				DefaultBuildSystemTypeForWorkspace = Original;
+				LastUpgradeCheck = 1100;
+				ORGANIZATIONNAME = "The Chromium Authors";
+				TargetAttributes = {
+					680049162280D368006DD6AB = {
+						CreatedOnToolsVersion = 10.2.1;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 97C146ED1CF9000F007C117D;
+					};
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						SystemCapabilities = {
+							com.apple.BackgroundModes = {
+								enabled = 1;
+							};
+						};
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+				680049162280D368006DD6AB /* image_picker_exampleTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		680049152280D368006DD6AB /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				680049272280D79A006DD6AB /* Assets.xcassets in Resources */,
+				9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */,
+				680049382280F2B9006DD6AB /* pngImage.png in Resources */,
+				680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Thin Binary";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+		};
+		95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+				"${PODS_ROOT}/../Flutter/Flutter.framework",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputPaths = (
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		9740EEB61CF901F6004384FC /* Run Script */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Run Script";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+		};
+		AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		680049132280D368006DD6AB /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9FC8F0EE229FB90B00C8D58F /* ImageUtilTests.m in Sources */,
+				F78AF3192342D9D7008449C7 /* ImagePickerTestImages.m in Sources */,
+				680049262280D736006DD6AB /* MetaDataUtilTests.m in Sources */,
+				68B9AF72243E4B3F00927CE4 /* ImagePickerPluginTests.m in Sources */,
+				68F4B464228B3AB500C25614 /* PhotoAssetUtilTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+				97C146F31CF9000F007C117D /* main.m in Sources */,
+				5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		6800491D2280D368006DD6AB /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 97C146ED1CF9000F007C117D /* Runner */;
+			targetProxy = 6800491C2280D368006DD6AB /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C146FB1CF9000F007C117D /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C147001CF9000F007C117D /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		6800491F2280D368006DD6AB /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				INFOPLIST_FILE = image_picker_exampleTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.google.transformTest.image-picker-exampleTests";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Debug;
+		};
+		680049202280D368006DD6AB /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				INFOPLIST_FILE = image_picker_exampleTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.google.transformTest.image-picker-exampleTests";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Release;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = "";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.imagePickerExample;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = "";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.imagePickerExample;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		6800491E2280D368006DD6AB /* Build configuration list for PBXNativeTarget "image_picker_exampleTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				6800491F2280D368006DD6AB /* Debug */,
+				680049202280D368006DD6AB /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}

+ 10 - 0
example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>

+ 97 - 0
example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1100"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+               BuildableName = "Runner.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "680049162280D368006DD6AB"
+               BuildableName = "image_picker_exampleTests.xctest"
+               BlueprintName = "image_picker_exampleTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 10 - 0
example/ios/Runner.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>

+ 8 - 0
example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 2 - 0
example/ios/Runner/.gitignore

@@ -0,0 +1,2 @@
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m

+ 10 - 0
example/ios/Runner/AppDelegate.h

@@ -0,0 +1,10 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end

+ 16 - 0
example/ios/Runner/AppDelegate.m

@@ -0,0 +1,16 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "AppDelegate.h"
+#include "GeneratedPluginRegistrant.h"
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application
+    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+  [GeneratedPluginRegistrant registerWithRegistry:self];
+  return [super application:application didFinishLaunchingWithOptions:launchOptions];
+}
+
+@end

+ 121 - 0
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,121 @@
+{
+  "images" : [
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "83.5x83.5",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-83.5x83.5@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "size" : "1024x1024",
+      "scale" : "1x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png


BIN
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png


+ 6 - 0
example/ios/Runner/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 27 - 0
example/ios/Runner/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 26 - 0
example/ios/Runner/Base.lproj/Main.storyboard

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--Flutter View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+</document>

+ 59 - 0
example/ios/Runner/Info.plist

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>image_picker_example</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>NSCameraUsageDescription</key>
+	<string>Used to demonstrate image picker plugin</string>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>Used to capture audio for image picker plugin</string>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string>Used to demonstrate image picker plugin</string>
+	<key>UIBackgroundModes</key>
+	<array>
+		<string>remote-notification</string>
+	</array>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>arm64</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UIViewControllerBasedStatusBarAppearance</key>
+	<false/>
+</dict>
+</plist>

+ 13 - 0
example/ios/Runner/main.m

@@ -0,0 +1,13 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+  @autoreleasepool {
+    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+  }
+}

BIN
example/ios/TestImages/gifImage.gif


BIN
example/ios/TestImages/jpgImage.jpg


BIN
example/ios/TestImages/pngImage.png


+ 22 - 0
example/ios/image_picker_exampleTests/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 395 - 0
example/lib/main.dart

@@ -0,0 +1,395 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: public_member_api_docs
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/basic.dart';
+import 'package:flutter/src/widgets/container.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:video_player/video_player.dart';
+
+void main() {
+  runApp(MyApp());
+}
+
+class MyApp extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Image Picker Demo',
+      home: MyHomePage(title: 'Image Picker Example'),
+    );
+  }
+}
+
+class MyHomePage extends StatefulWidget {
+  MyHomePage({Key key, this.title}) : super(key: key);
+
+  final String title;
+
+  @override
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  PickedFile _imageFile;
+  dynamic _pickImageError;
+  bool isVideo = false;
+  VideoPlayerController _controller;
+  String _retrieveDataError;
+
+  final ImagePicker _picker = ImagePicker();
+  final TextEditingController maxWidthController = TextEditingController();
+  final TextEditingController maxHeightController = TextEditingController();
+  final TextEditingController qualityController = TextEditingController();
+
+  Future<void> _playVideo(PickedFile file) async {
+    if (file != null && mounted) {
+      await _disposeVideoController();
+      if (kIsWeb) {
+        _controller = VideoPlayerController.network(file.path);
+      } else {
+        _controller = VideoPlayerController.file(File(file.path));
+      }
+      await _controller.setVolume(1.0);
+      await _controller.initialize();
+      await _controller.setLooping(true);
+      await _controller.play();
+      setState(() {});
+    }
+  }
+
+  void _onImageButtonPressed(ImageSource source, {BuildContext context}) async {
+    if (_controller != null) {
+      await _controller.setVolume(0.0);
+    }
+    if (isVideo) {
+      final PickedFile file = await _picker.getVideo(
+          source: source, maxDuration: const Duration(seconds: 10));
+      await _playVideo(file);
+    } else {
+      await _displayPickImageDialog(context,
+          (double maxWidth, double maxHeight, int quality) async {
+        try {
+          final pickedFile = await _picker.getImage(
+            source: source,
+            maxWidth: maxWidth,
+            maxHeight: maxHeight,
+            imageQuality: quality,
+          );
+          setState(() {
+            _imageFile = pickedFile;
+          });
+        } catch (e) {
+          setState(() {
+            _pickImageError = e;
+          });
+        }
+      });
+    }
+  }
+
+  @override
+  void deactivate() {
+    if (_controller != null) {
+      _controller.setVolume(0.0);
+      _controller.pause();
+    }
+    super.deactivate();
+  }
+
+  @override
+  void dispose() {
+    _disposeVideoController();
+    maxWidthController.dispose();
+    maxHeightController.dispose();
+    qualityController.dispose();
+    super.dispose();
+  }
+
+  Future<void> _disposeVideoController() async {
+    if (_controller != null) {
+      await _controller.dispose();
+      _controller = null;
+    }
+  }
+
+  Widget _previewVideo() {
+    final Text retrieveError = _getRetrieveErrorWidget();
+    if (retrieveError != null) {
+      return retrieveError;
+    }
+    if (_controller == null) {
+      return const Text(
+        'You have not yet picked a video',
+        textAlign: TextAlign.center,
+      );
+    }
+    return Padding(
+      padding: const EdgeInsets.all(10.0),
+      child: AspectRatioVideo(_controller),
+    );
+  }
+
+  Widget _previewImage() {
+    final Text retrieveError = _getRetrieveErrorWidget();
+    if (retrieveError != null) {
+      return retrieveError;
+    }
+    if (_imageFile != null) {
+      if (kIsWeb) {
+        // Why network?
+        // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform
+        return Image.network(_imageFile.path);
+      } else {
+        return Image.file(File(_imageFile.path));
+      }
+    } else if (_pickImageError != null) {
+      return Text(
+        'Pick image error: $_pickImageError',
+        textAlign: TextAlign.center,
+      );
+    } else {
+      return const Text(
+        'You have not yet picked an image.',
+        textAlign: TextAlign.center,
+      );
+    }
+  }
+
+  Future<void> retrieveLostData() async {
+    final LostData response = await _picker.getLostData();
+    if (response.isEmpty) {
+      return;
+    }
+    if (response.file != null) {
+      if (response.type == RetrieveType.video) {
+        isVideo = true;
+        await _playVideo(response.file);
+      } else {
+        isVideo = false;
+        setState(() {
+          _imageFile = response.file;
+        });
+      }
+    } else {
+      _retrieveDataError = response.exception.code;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Center(
+        child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android
+            ? FutureBuilder<void>(
+                future: retrieveLostData(),
+                builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
+                  switch (snapshot.connectionState) {
+                    case ConnectionState.none:
+                    case ConnectionState.waiting:
+                      return const Text(
+                        'You have not yet picked an image.',
+                        textAlign: TextAlign.center,
+                      );
+                    case ConnectionState.done:
+                      return isVideo ? _previewVideo() : _previewImage();
+                    default:
+                      if (snapshot.hasError) {
+                        return Text(
+                          'Pick image/video error: ${snapshot.error}}',
+                          textAlign: TextAlign.center,
+                        );
+                      } else {
+                        return const Text(
+                          'You have not yet picked an image.',
+                          textAlign: TextAlign.center,
+                        );
+                      }
+                  }
+                },
+              )
+            : (isVideo ? _previewVideo() : _previewImage()),
+      ),
+      floatingActionButton: Column(
+        mainAxisAlignment: MainAxisAlignment.end,
+        children: <Widget>[
+          FloatingActionButton(
+            onPressed: () {
+              isVideo = false;
+              _onImageButtonPressed(ImageSource.gallery, context: context);
+            },
+            heroTag: 'image0',
+            tooltip: 'Pick Image from gallery',
+            child: const Icon(Icons.photo_library),
+          ),
+          Padding(
+            padding: const EdgeInsets.only(top: 16.0),
+            child: FloatingActionButton(
+              onPressed: () {
+                isVideo = false;
+                _onImageButtonPressed(ImageSource.camera, context: context);
+              },
+              heroTag: 'image1',
+              tooltip: 'Take a Photo',
+              child: const Icon(Icons.camera_alt),
+            ),
+          ),
+          Padding(
+            padding: const EdgeInsets.only(top: 16.0),
+            child: FloatingActionButton(
+              backgroundColor: Colors.red,
+              onPressed: () {
+                isVideo = true;
+                _onImageButtonPressed(ImageSource.gallery);
+              },
+              heroTag: 'video0',
+              tooltip: 'Pick Video from gallery',
+              child: const Icon(Icons.video_library),
+            ),
+          ),
+          Padding(
+            padding: const EdgeInsets.only(top: 16.0),
+            child: FloatingActionButton(
+              backgroundColor: Colors.red,
+              onPressed: () {
+                isVideo = true;
+                _onImageButtonPressed(ImageSource.camera);
+              },
+              heroTag: 'video1',
+              tooltip: 'Take a Video',
+              child: const Icon(Icons.videocam),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Text _getRetrieveErrorWidget() {
+    if (_retrieveDataError != null) {
+      final Text result = Text(_retrieveDataError);
+      _retrieveDataError = null;
+      return result;
+    }
+    return null;
+  }
+
+  Future<void> _displayPickImageDialog(
+      BuildContext context, OnPickImageCallback onPick) async {
+    return showDialog(
+        context: context,
+        builder: (context) {
+          return AlertDialog(
+            title: Text('Add optional parameters'),
+            content: Column(
+              children: <Widget>[
+                TextField(
+                  controller: maxWidthController,
+                  keyboardType: TextInputType.numberWithOptions(decimal: true),
+                  decoration:
+                      InputDecoration(hintText: "Enter maxWidth if desired"),
+                ),
+                TextField(
+                  controller: maxHeightController,
+                  keyboardType: TextInputType.numberWithOptions(decimal: true),
+                  decoration:
+                      InputDecoration(hintText: "Enter maxHeight if desired"),
+                ),
+                TextField(
+                  controller: qualityController,
+                  keyboardType: TextInputType.number,
+                  decoration:
+                      InputDecoration(hintText: "Enter quality if desired"),
+                ),
+              ],
+            ),
+            actions: <Widget>[
+              FlatButton(
+                child: const Text('CANCEL'),
+                onPressed: () {
+                  Navigator.of(context).pop();
+                },
+              ),
+              FlatButton(
+                  child: const Text('PICK'),
+                  onPressed: () {
+                    double width = maxWidthController.text.isNotEmpty
+                        ? double.parse(maxWidthController.text)
+                        : null;
+                    double height = maxHeightController.text.isNotEmpty
+                        ? double.parse(maxHeightController.text)
+                        : null;
+                    int quality = qualityController.text.isNotEmpty
+                        ? int.parse(qualityController.text)
+                        : null;
+                    onPick(width, height, quality);
+                    Navigator.of(context).pop();
+                  }),
+            ],
+          );
+        });
+  }
+}
+
+typedef void OnPickImageCallback(
+    double maxWidth, double maxHeight, int quality);
+
+class AspectRatioVideo extends StatefulWidget {
+  AspectRatioVideo(this.controller);
+
+  final VideoPlayerController controller;
+
+  @override
+  AspectRatioVideoState createState() => AspectRatioVideoState();
+}
+
+class AspectRatioVideoState extends State<AspectRatioVideo> {
+  VideoPlayerController get controller => widget.controller;
+  bool initialized = false;
+
+  void _onVideoControllerUpdate() {
+    if (!mounted) {
+      return;
+    }
+    if (initialized != controller.value.initialized) {
+      initialized = controller.value.initialized;
+      setState(() {});
+    }
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    controller.addListener(_onVideoControllerUpdate);
+  }
+
+  @override
+  void dispose() {
+    controller.removeListener(_onVideoControllerUpdate);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (initialized) {
+      return Center(
+        child: AspectRatio(
+          aspectRatio: controller.value?.aspectRatio,
+          child: VideoPlayer(controller),
+        ),
+      );
+    } else {
+      return Container();
+    }
+  }
+}

+ 25 - 0
example/pubspec.yaml

@@ -0,0 +1,25 @@
+name: image_picker_example
+description: Demonstrates how to use the image_picker plugin.
+author: Flutter Team <flutter-dev@googlegroups.com>
+
+dependencies:
+  video_player: ^0.10.3
+  flutter:
+    sdk: flutter
+  flutter_plugin_android_lifecycle: ^1.0.2
+  image_picker:
+    path: ../
+  image_picker_for_web: ^0.1.0
+
+dev_dependencies:
+  flutter_driver:
+    sdk: flutter
+  e2e:  ^0.2.1
+  pedantic: ^1.8.0
+
+flutter:
+  uses-material-design: true
+
+environment:
+  sdk: ">=2.0.0-dev.28.0 <3.0.0"
+  flutter: ">=1.10.0 <2.0.0"

+ 15 - 0
example/test_driver/test/image_picker_e2e_test.dart

@@ -0,0 +1,15 @@
+// Copyright 2019, the Chromium project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+import 'package:flutter_driver/flutter_driver.dart';
+
+Future<void> main() async {
+  final FlutterDriver driver = await FlutterDriver.connect();
+  final String result =
+      await driver.requestData(null, timeout: const Duration(minutes: 1));
+  await driver.close();
+  exit(result == 'pass' ? 0 : 1);
+}

BIN
example/web/favicon.png


BIN
example/web/icons/Icon-192.png


BIN
example/web/icons/Icon-512.png


+ 33 - 0
example/web/index.html

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+  <meta name="description" content="An example of the image_picker on the web.">
+
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="example">
+  <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+  <!-- Favicon -->
+  <link rel="shortcut icon" type="image/png" href="favicon.png"/>
+
+  <title>url_launcher web example</title>
+  <link rel="manifest" href="manifest.json">
+</head>
+<body>
+  <!-- This script installs service_worker.js to provide PWA functionality to
+       application. For more information, see:
+       https://developers.google.com/web/fundamentals/primers/service-workers -->
+  <!-- <script>
+    if ('serviceWorker' in navigator) {
+      window.addEventListener('load', function () {
+        navigator.serviceWorker.register('flutter_service_worker.js');
+      });
+    }
+  </script> -->
+  <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>

+ 23 - 0
example/web/manifest.json

@@ -0,0 +1,23 @@
+{
+    "name": "image_picker example",
+    "short_name": "image_picker",
+    "start_url": ".",
+    "display": "minimal-ui",
+    "background_color": "#0175C2",
+    "theme_color": "#0175C2",
+    "description": "An example of the image_picker on the web.",
+    "orientation": "portrait-primary",
+    "prefer_related_applications": false,
+    "icons": [
+        {
+            "src": "icons/Icon-192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "icons/Icon-512.png",
+            "sizes": "512x512",
+            "type": "image/png"
+        }
+    ]
+}

+ 0 - 0
ios/Assets/.gitkeep


+ 32 - 0
ios/Classes/FLTImagePickerImageUtil.h

@@ -0,0 +1,32 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIFInfo : NSObject
+
+@property(strong, nonatomic, readonly) NSArray<UIImage *> *images;
+@property(assign, nonatomic, readonly) NSTimeInterval interval;
+
+- (instancetype)initWithImages:(NSArray<UIImage *> *)images interval:(NSTimeInterval)interval;
+
+@end
+
+@interface FLTImagePickerImageUtil : NSObject
+
++ (UIImage *)scaledImage:(UIImage *)image
+                maxWidth:(NSNumber *)maxWidth
+               maxHeight:(NSNumber *)maxHeight;
+
+// Resize all gif animation frames.
++ (GIFInfo *)scaledGIFImage:(NSData *)data
+                   maxWidth:(NSNumber *)maxWidth
+                  maxHeight:(NSNumber *)maxHeight;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 147 - 0
ios/Classes/FLTImagePickerImageUtil.m

@@ -0,0 +1,147 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "FLTImagePickerImageUtil.h"
+#import <MobileCoreServices/MobileCoreServices.h>
+
+@interface GIFInfo ()
+
+@property(strong, nonatomic, readwrite) NSArray<UIImage *> *images;
+@property(assign, nonatomic, readwrite) NSTimeInterval interval;
+
+@end
+
+@implementation GIFInfo
+
+- (instancetype)initWithImages:(NSArray<UIImage *> *)images interval:(NSTimeInterval)interval;
+{
+  self = [super init];
+  if (self) {
+    self.images = images;
+    self.interval = interval;
+  }
+  return self;
+}
+
+@end
+
+@implementation FLTImagePickerImageUtil : NSObject
+
++ (UIImage *)scaledImage:(UIImage *)image
+                maxWidth:(NSNumber *)maxWidth
+               maxHeight:(NSNumber *)maxHeight {
+  double originalWidth = image.size.width;
+  double originalHeight = image.size.height;
+
+  bool hasMaxWidth = maxWidth != (id)[NSNull null];
+  bool hasMaxHeight = maxHeight != (id)[NSNull null];
+
+  double width = hasMaxWidth ? MIN([maxWidth doubleValue], originalWidth) : originalWidth;
+  double height = hasMaxHeight ? MIN([maxHeight doubleValue], originalHeight) : originalHeight;
+
+  bool shouldDownscaleWidth = hasMaxWidth && [maxWidth doubleValue] < originalWidth;
+  bool shouldDownscaleHeight = hasMaxHeight && [maxHeight doubleValue] < originalHeight;
+  bool shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight;
+
+  if (shouldDownscale) {
+    double downscaledWidth = floor((height / originalHeight) * originalWidth);
+    double downscaledHeight = floor((width / originalWidth) * originalHeight);
+
+    if (width < height) {
+      if (!hasMaxWidth) {
+        width = downscaledWidth;
+      } else {
+        height = downscaledHeight;
+      }
+    } else if (height < width) {
+      if (!hasMaxHeight) {
+        height = downscaledHeight;
+      } else {
+        width = downscaledWidth;
+      }
+    } else {
+      if (originalWidth < originalHeight) {
+        width = downscaledWidth;
+      } else if (originalHeight < originalWidth) {
+        height = downscaledHeight;
+      }
+    }
+  }
+
+  // Scaling the image always rotate itself based on the current imageOrientation of the original
+  // Image. Set to orientationUp for the orignal image before scaling, so the scaled image doesn't
+  // mess up with the pixels.
+  UIImage *imageToScale = [UIImage imageWithCGImage:image.CGImage
+                                              scale:1
+                                        orientation:UIImageOrientationUp];
+
+  // The image orientation is manually set to UIImageOrientationUp which swapped the aspect ratio in
+  // some scenarios. For example, when the original image has orientation left, the horizontal
+  // pixels should be scaled to `width` and the vertical pixels should be scaled to `height`. After
+  // setting the orientation to up, we end up scaling the horizontal pixels to `height` and vertical
+  // to `width`. Below swap will solve this issue.
+  if ([image imageOrientation] == UIImageOrientationLeft ||
+      [image imageOrientation] == UIImageOrientationRight ||
+      [image imageOrientation] == UIImageOrientationLeftMirrored ||
+      [image imageOrientation] == UIImageOrientationRightMirrored) {
+    double temp = width;
+    width = height;
+    height = temp;
+  }
+
+  UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0);
+  [imageToScale drawInRect:CGRectMake(0, 0, width, height)];
+
+  UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
+  UIGraphicsEndImageContext();
+  return scaledImage;
+}
+
++ (GIFInfo *)scaledGIFImage:(NSData *)data
+                   maxWidth:(NSNumber *)maxWidth
+                  maxHeight:(NSNumber *)maxHeight {
+  NSMutableDictionary<NSString *, id> *options = [NSMutableDictionary dictionary];
+  options[(NSString *)kCGImageSourceShouldCache] = @(YES);
+  options[(NSString *)kCGImageSourceTypeIdentifierHint] = (NSString *)kUTTypeGIF;
+
+  CGImageSourceRef imageSource =
+      CGImageSourceCreateWithData((CFDataRef)data, (CFDictionaryRef)options);
+
+  size_t numberOfFrames = CGImageSourceGetCount(imageSource);
+  NSMutableArray<UIImage *> *images = [NSMutableArray arrayWithCapacity:numberOfFrames];
+
+  NSTimeInterval interval = 0.0;
+  for (size_t index = 0; index < numberOfFrames; index++) {
+    CGImageRef imageRef =
+        CGImageSourceCreateImageAtIndex(imageSource, index, (CFDictionaryRef)options);
+
+    NSDictionary *properties = (NSDictionary *)CFBridgingRelease(
+        CGImageSourceCopyPropertiesAtIndex(imageSource, index, NULL));
+    NSDictionary *gifProperties = properties[(NSString *)kCGImagePropertyGIFDictionary];
+
+    NSNumber *delay = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
+    if (delay == nil) {
+      delay = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
+    }
+
+    if (interval == 0.0) {
+      interval = [delay doubleValue];
+    }
+
+    UIImage *image = [UIImage imageWithCGImage:imageRef scale:1.0 orientation:UIImageOrientationUp];
+    image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight];
+
+    [images addObject:image];
+
+    CGImageRelease(imageRef);
+  }
+
+  CFRelease(imageSource);
+
+  GIFInfo *info = [[GIFInfo alloc] initWithImages:images interval:interval];
+
+  return info;
+}
+
+@end

+ 44 - 0
ios/Classes/FLTImagePickerMetaDataUtil.h

@@ -0,0 +1,44 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef enum : NSUInteger {
+  FLTImagePickerMIMETypePNG,
+  FLTImagePickerMIMETypeJPEG,
+  FLTImagePickerMIMETypeGIF,
+  FLTImagePickerMIMETypeOther,
+} FLTImagePickerMIMEType;
+
+extern NSString *const kFLTImagePickerDefaultSuffix;
+extern const FLTImagePickerMIMEType kFLTImagePickerMIMETypeDefault;
+
+@interface FLTImagePickerMetaDataUtil : NSObject
+
+// Retrieve MIME type by reading the image data. We currently only support some popular types.
++ (FLTImagePickerMIMEType)getImageMIMETypeFromImageData:(NSData *)imageData;
+
+// Get corresponding surfix from type.
++ (nullable NSString *)imageTypeSuffixFromType:(FLTImagePickerMIMEType)type;
+
++ (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData;
+
++ (NSData *)updateMetaData:(NSDictionary *)metaData toImage:(NSData *)imageData;
+
+// Converting UIImage to a NSData with the type proveide.
+//
+// The quality is for JPEG type only, it defaults to 1. It throws exception if setting a non-nil
+// quality with type other than FLTImagePickerMIMETypeJPEG. Converting UIImage to
+// FLTImagePickerMIMETypeGIF or FLTImagePickerMIMETypeTIFF is not supported in iOS. This
+// method throws exception if trying to do so.
++ (nonnull NSData *)convertImage:(nonnull UIImage *)image
+                       usingType:(FLTImagePickerMIMEType)type
+                         quality:(nullable NSNumber *)quality;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 88 - 0
ios/Classes/FLTImagePickerMetaDataUtil.m

@@ -0,0 +1,88 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "FLTImagePickerMetaDataUtil.h"
+#import <Photos/Photos.h>
+
+static const uint8_t kFirstByteJPEG = 0xFF;
+static const uint8_t kFirstBytePNG = 0x89;
+static const uint8_t kFirstByteGIF = 0x47;
+
+NSString *const kFLTImagePickerDefaultSuffix = @".jpg";
+const FLTImagePickerMIMEType kFLTImagePickerMIMETypeDefault = FLTImagePickerMIMETypeJPEG;
+
+@implementation FLTImagePickerMetaDataUtil
+
++ (FLTImagePickerMIMEType)getImageMIMETypeFromImageData:(NSData *)imageData {
+  uint8_t firstByte;
+  [imageData getBytes:&firstByte length:1];
+  switch (firstByte) {
+    case kFirstByteJPEG:
+      return FLTImagePickerMIMETypeJPEG;
+    case kFirstBytePNG:
+      return FLTImagePickerMIMETypePNG;
+    case kFirstByteGIF:
+      return FLTImagePickerMIMETypeGIF;
+  }
+  return FLTImagePickerMIMETypeOther;
+}
+
++ (NSString *)imageTypeSuffixFromType:(FLTImagePickerMIMEType)type {
+  switch (type) {
+    case FLTImagePickerMIMETypeJPEG:
+      return @".jpg";
+    case FLTImagePickerMIMETypePNG:
+      return @".png";
+    case FLTImagePickerMIMETypeGIF:
+      return @".gif";
+    default:
+      return nil;
+  }
+}
+
++ (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData {
+  CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
+  NSDictionary *metadata =
+      (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(source, 0, NULL));
+  CFRelease(source);
+  return metadata;
+}
+
++ (NSData *)updateMetaData:(NSDictionary *)metaData toImage:(NSData *)imageData {
+  NSMutableData *mutableData = [NSMutableData data];
+  CGImageSourceRef cgImage = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
+  CGImageDestinationRef destination = CGImageDestinationCreateWithData(
+      (__bridge CFMutableDataRef)mutableData, CGImageSourceGetType(cgImage), 1, nil);
+  CGImageDestinationAddImageFromSource(destination, cgImage, 0, (__bridge CFDictionaryRef)metaData);
+  CGImageDestinationFinalize(destination);
+  CFRelease(cgImage);
+  CFRelease(destination);
+  return mutableData;
+}
+
++ (NSData *)convertImage:(UIImage *)image
+               usingType:(FLTImagePickerMIMEType)type
+                 quality:(nullable NSNumber *)quality {
+  if (quality && type != FLTImagePickerMIMETypeJPEG) {
+    NSLog(@"image_picker: compressing is not supported for type %@. Returning the image with "
+          @"original quality",
+          [FLTImagePickerMetaDataUtil imageTypeSuffixFromType:type]);
+  }
+
+  switch (type) {
+    case FLTImagePickerMIMETypeJPEG: {
+      CGFloat qualityFloat = (quality != nil) ? quality.floatValue : 1;
+      return UIImageJPEGRepresentation(image, qualityFloat);
+    }
+    case FLTImagePickerMIMETypePNG:
+      return UIImagePNGRepresentation(image);
+    default: {
+      // converts to JPEG by default.
+      CGFloat qualityFloat = (quality != nil) ? quality.floatValue : 1;
+      return UIImageJPEGRepresentation(image, qualityFloat);
+    }
+  }
+}
+
+@end

+ 31 - 0
ios/Classes/FLTImagePickerPhotoAssetUtil.h

@@ -0,0 +1,31 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Foundation/Foundation.h>
+#import <Photos/Photos.h>
+
+#import "FLTImagePickerImageUtil.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FLTImagePickerPhotoAssetUtil : NSObject
+
++ (nullable PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info;
+
+// Save image with correct meta data and extention copied from the original asset.
+// maxWidth and maxHeight are used only for GIF images.
++ (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData
+                                       image:(UIImage *)image
+                                    maxWidth:(nullable NSNumber *)maxWidth
+                                   maxHeight:(nullable NSNumber *)maxHeight
+                                imageQuality:(nullable NSNumber *)imageQuality;
+
+// Save image with correct meta data and extention copied from image picker result info.
++ (NSString *)saveImageWithPickerInfo:(nullable NSDictionary *)info
+                                image:(UIImage *)image
+                         imageQuality:(nullable NSNumber *)imageQuality;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 142 - 0
ios/Classes/FLTImagePickerPhotoAssetUtil.m

@@ -0,0 +1,142 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "FLTImagePickerPhotoAssetUtil.h"
+#import "FLTImagePickerImageUtil.h"
+#import "FLTImagePickerMetaDataUtil.h"
+
+#import <MobileCoreServices/MobileCoreServices.h>
+
+@implementation FLTImagePickerPhotoAssetUtil
+
++ (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info {
+  if (@available(iOS 11, *)) {
+    return [info objectForKey:UIImagePickerControllerPHAsset];
+  }
+  NSURL *referenceURL = [info objectForKey:UIImagePickerControllerReferenceURL];
+  if (!referenceURL) {
+    return nil;
+  }
+  PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithALAssetURLs:@[ referenceURL ]
+                                                                 options:nil];
+  return result.firstObject;
+}
+
++ (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData
+                                       image:(UIImage *)image
+                                    maxWidth:(NSNumber *)maxWidth
+                                   maxHeight:(NSNumber *)maxHeight
+                                imageQuality:(NSNumber *)imageQuality {
+  NSString *suffix = kFLTImagePickerDefaultSuffix;
+  FLTImagePickerMIMEType type = kFLTImagePickerMIMETypeDefault;
+  NSDictionary *metaData = nil;
+  // Getting the image type from the original image data if necessary.
+  if (originalImageData) {
+    type = [FLTImagePickerMetaDataUtil getImageMIMETypeFromImageData:originalImageData];
+    suffix =
+        [FLTImagePickerMetaDataUtil imageTypeSuffixFromType:type] ?: kFLTImagePickerDefaultSuffix;
+    metaData = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:originalImageData];
+  }
+  if (type == FLTImagePickerMIMETypeGIF) {
+    GIFInfo *gifInfo = [FLTImagePickerImageUtil scaledGIFImage:originalImageData
+                                                      maxWidth:maxWidth
+                                                     maxHeight:maxHeight];
+
+    return [self saveImageWithMetaData:metaData gifInfo:gifInfo suffix:suffix];
+  } else {
+    return [self saveImageWithMetaData:metaData
+                                 image:image
+                                suffix:suffix
+                                  type:type
+                          imageQuality:imageQuality];
+  }
+}
+
++ (NSString *)saveImageWithPickerInfo:(nullable NSDictionary *)info
+                                image:(UIImage *)image
+                         imageQuality:(NSNumber *)imageQuality {
+  NSDictionary *metaData = info[UIImagePickerControllerMediaMetadata];
+  return [self saveImageWithMetaData:metaData
+                               image:image
+                              suffix:kFLTImagePickerDefaultSuffix
+                                type:kFLTImagePickerMIMETypeDefault
+                        imageQuality:imageQuality];
+}
+
++ (NSString *)saveImageWithMetaData:(NSDictionary *)metaData
+                            gifInfo:(GIFInfo *)gifInfo
+                             suffix:(NSString *)suffix {
+  NSString *path = [self temporaryFilePath:suffix];
+  return [self saveImageWithMetaData:metaData gifInfo:gifInfo path:path];
+}
+
++ (NSString *)saveImageWithMetaData:(NSDictionary *)metaData
+                              image:(UIImage *)image
+                             suffix:(NSString *)suffix
+                               type:(FLTImagePickerMIMEType)type
+                       imageQuality:(NSNumber *)imageQuality {
+  NSData *data = [FLTImagePickerMetaDataUtil convertImage:image
+                                                usingType:type
+                                                  quality:imageQuality];
+  if (metaData) {
+    data = [FLTImagePickerMetaDataUtil updateMetaData:metaData toImage:data];
+  }
+
+  return [self createFile:data suffix:suffix];
+}
+
++ (NSString *)saveImageWithMetaData:(NSDictionary *)metaData
+                            gifInfo:(GIFInfo *)gifInfo
+                               path:(NSString *)path {
+  CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
+      (CFURLRef)[NSURL fileURLWithPath:path], kUTTypeGIF, gifInfo.images.count, NULL);
+
+  NSDictionary *frameProperties = [NSDictionary
+      dictionaryWithObject:[NSDictionary
+                               dictionaryWithObject:[NSNumber numberWithFloat:gifInfo.interval]
+                                             forKey:(NSString *)kCGImagePropertyGIFDelayTime]
+                    forKey:(NSString *)kCGImagePropertyGIFDictionary];
+
+  NSMutableDictionary *gifMetaProperties = [NSMutableDictionary dictionaryWithDictionary:metaData];
+  NSMutableDictionary *gifProperties =
+      (NSMutableDictionary *)gifMetaProperties[(NSString *)kCGImagePropertyGIFDictionary];
+  if (gifMetaProperties == nil) {
+    gifProperties = [NSMutableDictionary dictionary];
+  }
+
+  gifProperties[(NSString *)kCGImagePropertyGIFLoopCount] = [NSNumber numberWithFloat:0];
+
+  CGImageDestinationSetProperties(destination, (CFDictionaryRef)gifMetaProperties);
+
+  for (NSInteger index = 0; index < gifInfo.images.count; index++) {
+    UIImage *image = (UIImage *)[gifInfo.images objectAtIndex:index];
+    CGImageDestinationAddImage(destination, image.CGImage, (CFDictionaryRef)frameProperties);
+  }
+
+  CGImageDestinationFinalize(destination);
+  CFRelease(destination);
+
+  return path;
+}
+
++ (NSString *)temporaryFilePath:(NSString *)suffix {
+  NSString *fileExtension = [@"image_picker_%@" stringByAppendingString:suffix];
+  NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
+  NSString *tmpFile = [NSString stringWithFormat:fileExtension, guid];
+  NSString *tmpDirectory = NSTemporaryDirectory();
+  NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile];
+  return tmpPath;
+}
+
++ (NSString *)createFile:(NSData *)data suffix:(NSString *)suffix {
+  NSString *tmpPath = [self temporaryFilePath:suffix];
+  if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:data attributes:nil]) {
+    return tmpPath;
+  } else {
+    nil;
+  }
+  return tmpPath;
+}
+
+@end

+ 13 - 0
ios/Classes/FLTImagePickerPlugin.h

@@ -0,0 +1,13 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Flutter/Flutter.h>
+
+@interface FLTImagePickerPlugin : NSObject <FlutterPlugin>
+
+// For testing only.
+- (UIImagePickerController *)getImagePickerController;
+- (UIViewController *)viewControllerWithWindow:(UIWindow *)window;
+
+@end

+ 385 - 0
ios/Classes/FLTImagePickerPlugin.m

@@ -0,0 +1,385 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "FLTImagePickerPlugin.h"
+
+#import <AVFoundation/AVFoundation.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+#import <Photos/Photos.h>
+#import <UIKit/UIKit.h>
+
+#import "FLTImagePickerImageUtil.h"
+#import "FLTImagePickerMetaDataUtil.h"
+#import "FLTImagePickerPhotoAssetUtil.h"
+
+@interface FLTImagePickerPlugin () <UINavigationControllerDelegate, UIImagePickerControllerDelegate>
+
+@property(copy, nonatomic) FlutterResult result;
+
+@end
+
+static const int SOURCE_CAMERA = 0;
+static const int SOURCE_GALLERY = 1;
+
+@implementation FLTImagePickerPlugin {
+  NSDictionary *_arguments;
+  UIImagePickerController *_imagePickerController;
+  UIImagePickerControllerCameraDevice _device;
+}
+
++ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
+  FlutterMethodChannel *channel =
+      [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker"
+                                  binaryMessenger:[registrar messenger]];
+  FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new];
+  [registrar addMethodCallDelegate:instance channel:channel];
+}
+
+- (UIImagePickerController *)getImagePickerController {
+  return _imagePickerController;
+}
+
+- (UIViewController *)viewControllerWithWindow:(UIWindow *)window {
+  UIWindow *windowToUse = window;
+  if (windowToUse == nil) {
+    for (UIWindow *window in [UIApplication sharedApplication].windows) {
+      if (window.isKeyWindow) {
+        windowToUse = window;
+        break;
+      }
+    }
+  }
+
+  UIViewController *topController = windowToUse.rootViewController;
+  while (topController.presentedViewController) {
+    topController = topController.presentedViewController;
+  }
+  return topController;
+}
+
+- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
+  if (self.result) {
+    self.result([FlutterError errorWithCode:@"multiple_request"
+                                    message:@"Cancelled by a second request"
+                                    details:nil]);
+    self.result = nil;
+  }
+
+  if ([@"pickImage" isEqualToString:call.method]) {
+    _imagePickerController = [[UIImagePickerController alloc] init];
+    _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
+    _imagePickerController.delegate = self;
+    _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ];
+
+    self.result = result;
+    _arguments = call.arguments;
+
+    int imageSource = [[_arguments objectForKey:@"source"] intValue];
+
+    switch (imageSource) {
+      case SOURCE_CAMERA: {
+        NSInteger cameraDevice = [[_arguments objectForKey:@"cameraDevice"] intValue];
+        _device = (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront
+                                      : UIImagePickerControllerCameraDeviceRear;
+        [self checkCameraAuthorization];
+        break;
+      }
+      case SOURCE_GALLERY:
+        [self checkPhotoAuthorization];
+        break;
+      default:
+        result([FlutterError errorWithCode:@"invalid_source"
+                                   message:@"Invalid image source."
+                                   details:nil]);
+        break;
+    }
+  } else if ([@"pickVideo" isEqualToString:call.method]) {
+    _imagePickerController = [[UIImagePickerController alloc] init];
+    _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
+    _imagePickerController.delegate = self;
+    _imagePickerController.mediaTypes = @[
+      (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo,
+      (NSString *)kUTTypeMPEG4
+    ];
+    _imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh;
+
+    self.result = result;
+    _arguments = call.arguments;
+
+    int imageSource = [[_arguments objectForKey:@"source"] intValue];
+    if ([[_arguments objectForKey:@"maxDuration"] isKindOfClass:[NSNumber class]]) {
+      NSTimeInterval max = [[_arguments objectForKey:@"maxDuration"] doubleValue];
+      _imagePickerController.videoMaximumDuration = max;
+    }
+
+    switch (imageSource) {
+      case SOURCE_CAMERA:
+        [self checkCameraAuthorization];
+        break;
+      case SOURCE_GALLERY:
+        [self checkPhotoAuthorization];
+        break;
+      default:
+        result([FlutterError errorWithCode:@"invalid_source"
+                                   message:@"Invalid video source."
+                                   details:nil]);
+        break;
+    }
+  } else {
+    result(FlutterMethodNotImplemented);
+  }
+}
+
+- (void)showCamera {
+  @synchronized(self) {
+    if (_imagePickerController.beingPresented) {
+      return;
+    }
+  }
+  // Camera is not available on simulators
+  if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] &&
+      [UIImagePickerController isCameraDeviceAvailable:_device]) {
+    _imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
+    _imagePickerController.cameraDevice = _device;
+    [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController
+                                                      animated:YES
+                                                    completion:nil];
+  } else {
+    [[[UIAlertView alloc] initWithTitle:@"Error"
+                                message:@"Camera not available."
+                               delegate:nil
+                      cancelButtonTitle:@"OK"
+                      otherButtonTitles:nil] show];
+    self.result(nil);
+    self.result = nil;
+    _arguments = nil;
+  }
+}
+
+- (void)checkCameraAuthorization {
+  AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
+
+  switch (status) {
+    case AVAuthorizationStatusAuthorized:
+      [self showCamera];
+      break;
+    case AVAuthorizationStatusNotDetermined: {
+      [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
+                               completionHandler:^(BOOL granted) {
+                                 if (granted) {
+                                   dispatch_async(dispatch_get_main_queue(), ^{
+                                     if (granted) {
+                                       [self showCamera];
+                                     }
+                                   });
+                                 } else {
+                                   dispatch_async(dispatch_get_main_queue(), ^{
+                                     [self errorNoCameraAccess:AVAuthorizationStatusDenied];
+                                   });
+                                 }
+                               }];
+    }; break;
+    case AVAuthorizationStatusDenied:
+    case AVAuthorizationStatusRestricted:
+    default:
+      [self errorNoCameraAccess:status];
+      break;
+  }
+}
+
+- (void)checkPhotoAuthorization {
+  PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
+  switch (status) {
+    case PHAuthorizationStatusNotDetermined: {
+      [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
+        if (status == PHAuthorizationStatusAuthorized) {
+          dispatch_async(dispatch_get_main_queue(), ^{
+            [self showPhotoLibrary];
+          });
+        } else {
+          [self errorNoPhotoAccess:status];
+        }
+      }];
+      break;
+    }
+    case PHAuthorizationStatusAuthorized:
+      [self showPhotoLibrary];
+      break;
+    case PHAuthorizationStatusDenied:
+    case PHAuthorizationStatusRestricted:
+    default:
+      [self errorNoPhotoAccess:status];
+      break;
+  }
+}
+
+- (void)errorNoCameraAccess:(AVAuthorizationStatus)status {
+  switch (status) {
+    case AVAuthorizationStatusRestricted:
+      self.result([FlutterError errorWithCode:@"camera_access_restricted"
+                                      message:@"The user is not allowed to use the camera."
+                                      details:nil]);
+      break;
+    case AVAuthorizationStatusDenied:
+    default:
+      self.result([FlutterError errorWithCode:@"camera_access_denied"
+                                      message:@"The user did not allow camera access."
+                                      details:nil]);
+      break;
+  }
+}
+
+- (void)errorNoPhotoAccess:(PHAuthorizationStatus)status {
+  switch (status) {
+    case PHAuthorizationStatusRestricted:
+      self.result([FlutterError errorWithCode:@"photo_access_restricted"
+                                      message:@"The user is not allowed to use the photo."
+                                      details:nil]);
+      break;
+    case PHAuthorizationStatusDenied:
+    default:
+      self.result([FlutterError errorWithCode:@"photo_access_denied"
+                                      message:@"The user did not allow photo access."
+                                      details:nil]);
+      break;
+  }
+}
+
+- (void)showPhotoLibrary {
+  // No need to check if SourceType is available. It always is.
+  _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
+  [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController
+                                                    animated:YES
+                                                  completion:nil];
+}
+
+- (void)imagePickerController:(UIImagePickerController *)picker
+    didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
+  NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL];
+  [_imagePickerController dismissViewControllerAnimated:YES completion:nil];
+  // The method dismissViewControllerAnimated does not immediately prevent
+  // further didFinishPickingMediaWithInfo invocations. A nil check is necessary
+  // to prevent below code to be unwantly executed multiple times and cause a
+  // crash.
+  if (!self.result) {
+    return;
+  }
+  if (videoURL != nil) {
+    if (@available(iOS 13.0, *)) {
+      NSString *fileName = [videoURL lastPathComponent];
+      NSURL *destination =
+          [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]];
+
+      if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) {
+        NSError *error;
+        if (![[videoURL path] isEqualToString:[destination path]]) {
+          [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error];
+
+          if (error) {
+            self.result([FlutterError errorWithCode:@"flutter_image_picker_copy_video_error"
+                                            message:@"Could not cache the video file."
+                                            details:nil]);
+            self.result = nil;
+            return;
+          }
+        }
+        videoURL = destination;
+      }
+    }
+    self.result(videoURL.path);
+    self.result = nil;
+    _arguments = nil;
+  } else {
+    UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage];
+    if (image == nil) {
+      image = [info objectForKey:UIImagePickerControllerOriginalImage];
+    }
+
+    NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"];
+    NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"];
+    NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"];
+
+    if (![imageQuality isKindOfClass:[NSNumber class]]) {
+      imageQuality = @1;
+    } else if (imageQuality.intValue < 0 || imageQuality.intValue > 100) {
+      imageQuality = [NSNumber numberWithInt:1];
+    } else {
+      imageQuality = @([imageQuality floatValue] / 100);
+    }
+
+    if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) {
+      image = [FLTImagePickerImageUtil scaledImage:image maxWidth:maxWidth maxHeight:maxHeight];
+    }
+
+    PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info];
+    if (!originalAsset) {
+      // Image picked without an original asset (e.g. User took a photo directly)
+      [self saveImageWithPickerInfo:info image:image imageQuality:imageQuality];
+    } else {
+      __weak typeof(self) weakSelf = self;
+      [[PHImageManager defaultManager]
+          requestImageDataForAsset:originalAsset
+                           options:nil
+                     resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI,
+                                     UIImageOrientation orientation, NSDictionary *_Nullable info) {
+                       // maxWidth and maxHeight are used only for GIF images.
+                       [weakSelf saveImageWithOriginalImageData:imageData
+                                                          image:image
+                                                       maxWidth:maxWidth
+                                                      maxHeight:maxHeight
+                                                   imageQuality:imageQuality];
+                     }];
+    }
+  }
+}
+
+- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
+  [_imagePickerController dismissViewControllerAnimated:YES completion:nil];
+  if (!self.result) {
+    return;
+  }
+  self.result(nil);
+  self.result = nil;
+  _arguments = nil;
+}
+
+- (void)saveImageWithOriginalImageData:(NSData *)originalImageData
+                                 image:(UIImage *)image
+                              maxWidth:(NSNumber *)maxWidth
+                             maxHeight:(NSNumber *)maxHeight
+                          imageQuality:(NSNumber *)imageQuality {
+  NSString *savedPath =
+      [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:originalImageData
+                                                             image:image
+                                                          maxWidth:maxWidth
+                                                         maxHeight:maxHeight
+                                                      imageQuality:imageQuality];
+  [self handleSavedPath:savedPath];
+}
+
+- (void)saveImageWithPickerInfo:(NSDictionary *)info
+                          image:(UIImage *)image
+                   imageQuality:(NSNumber *)imageQuality {
+  NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info
+                                                                        image:image
+                                                                 imageQuality:imageQuality];
+  [self handleSavedPath:savedPath];
+}
+
+- (void)handleSavedPath:(NSString *)path {
+  if (!self.result) {
+    return;
+  }
+  if (path) {
+    self.result(path);
+  } else {
+    self.result([FlutterError errorWithCode:@"create_error"
+                                    message:@"Temporary file could not be created"
+                                    details:nil]);
+  }
+  self.result = nil;
+  _arguments = nil;
+}
+
+@end

+ 152 - 0
ios/Tests/ImagePickerPluginTests.m

@@ -0,0 +1,152 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ImagePickerTestImages.h"
+
+@import image_picker;
+@import XCTest;
+
+@interface MockViewController : UIViewController
+@property(nonatomic, retain) UIViewController *mockPresented;
+@end
+
+@implementation MockViewController
+@synthesize mockPresented;
+
+- (UIViewController *)presentedViewController {
+  return mockPresented;
+}
+
+@end
+
+@interface FLTImagePickerPlugin (Test)
+@property(copy, nonatomic) FlutterResult result;
+- (void)handleSavedPath:(NSString *)path;
+- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
+@end
+
+@interface ImagePickerPluginTests : XCTestCase
+@end
+
+@implementation ImagePickerPluginTests
+
+#pragma mark - Test camera devices, no op on simulators
+- (void)testPluginPickImageDeviceBack {
+  if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
+    return;
+  }
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  FlutterMethodCall *call =
+      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
+                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable r){
+                    }];
+  XCTAssertEqual([plugin getImagePickerController].cameraDevice,
+                 UIImagePickerControllerCameraDeviceRear);
+}
+
+- (void)testPluginPickImageDeviceFront {
+  if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
+    return;
+  }
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  FlutterMethodCall *call =
+      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
+                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable r){
+                    }];
+  XCTAssertEqual([plugin getImagePickerController].cameraDevice,
+                 UIImagePickerControllerCameraDeviceFront);
+}
+
+- (void)testPluginPickVideoDeviceBack {
+  if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
+    return;
+  }
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  FlutterMethodCall *call =
+      [FlutterMethodCall methodCallWithMethodName:@"pickVideo"
+                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable r){
+                    }];
+  XCTAssertEqual([plugin getImagePickerController].cameraDevice,
+                 UIImagePickerControllerCameraDeviceRear);
+}
+
+- (void)testPluginPickImageDeviceCancelClickMultipleTimes {
+  if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
+    return;
+  }
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  FlutterMethodCall *call =
+      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
+                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable r){
+                    }];
+  plugin.result = ^(id result) {
+
+  };
+  [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]];
+  [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]];
+}
+
+- (void)testPluginPickVideoDeviceFront {
+  if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
+    return;
+  }
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  FlutterMethodCall *call =
+      [FlutterMethodCall methodCallWithMethodName:@"pickVideo"
+                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable r){
+                    }];
+  XCTAssertEqual([plugin getImagePickerController].cameraDevice,
+                 UIImagePickerControllerCameraDeviceFront);
+}
+
+#pragma mark - Test video duration
+- (void)testPickingVideoWithDuration {
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  FlutterMethodCall *call = [FlutterMethodCall
+      methodCallWithMethodName:@"pickVideo"
+                     arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable r){
+                    }];
+  XCTAssertEqual([plugin getImagePickerController].videoMaximumDuration, 95);
+}
+
+- (void)testPluginPickImageSelectMultipleTimes {
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  FlutterMethodCall *call =
+      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
+                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable r){
+                    }];
+  plugin.result = ^(id result) {
+
+  };
+  [plugin handleSavedPath:@"test"];
+  [plugin handleSavedPath:@"test"];
+}
+
+- (void)testViewController {
+  UIWindow *window = [UIWindow new];
+  MockViewController *vc1 = [MockViewController new];
+  window.rootViewController = vc1;
+
+  UIViewController *vc2 = [UIViewController new];
+  vc1.mockPresented = vc2;
+
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  XCTAssertEqual([plugin viewControllerWithWindow:window], vc2);
+}
+
+@end

+ 17 - 0
ios/Tests/ImagePickerTestImages.h

@@ -0,0 +1,17 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@import Foundation;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface ImagePickerTestImages : NSObject
+
+@property(class, copy, readonly) NSData *JPGTestData;
+@property(class, copy, readonly) NSData *PNGTestData;
+@property(class, copy, readonly) NSData *GIFTestData;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 122 - 0
ios/Tests/ImagePickerTestImages.m

@@ -0,0 +1,122 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ImagePickerTestImages.h"
+
+@implementation ImagePickerTestImages
+
++ (NSData*)JPGTestData {
+  NSBundle* bundle = [NSBundle bundleForClass:self];
+  NSURL* url = [bundle URLForResource:@"jpgImage" withExtension:@"jpg"];
+  NSData* data = [NSData dataWithContentsOfURL:url];
+  if (!data.length) {
+    // When the tests are run outside the example project (podspec lint) the image may not be
+    // embedded in the test bundle. Fall back to the base64 string representation of the jpg.
+    data = [[NSData alloc]
+        initWithBase64EncodedString:
+            @"/9j/4AAQSkZJRgABAQAALgAuAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABA"
+             "AAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAAAuAAAAAQAAAC4AAAABAAOg"
+             "AQADAAAAAQABAACgAgAEAAAAAQAAAAygAwAEAAAAAQAAAAcAAAAA/+EJc2h0dHA6Ly9ucy5hZG9iZS5jb20"
+             "veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz"
+             "4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiP"
+             "iA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucy"
+             "MiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZ"
+             "G9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHBob3Rvc2hvcDpDcmVkaXQ9IsKpIEdvb2dsZSIvPiA8L3JkZjpSR"
+             "EY+IDwveDp4bXBtZXRhPiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI"
+             "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDw/eHBhY2tldCBlbmQ9In"
+             "ciPz4A/+0AVlBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAdHAFaAAMbJUccAgAAAgACHAJuAAnCqSBHb29nbG"
+             "UAOEJJTQQlAAAAAAAQmkt2IF3PgNJVMGnV2zijEf/AABEIAAcADAMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQA"
+             "AAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQ"
+             "gjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h"
+             "5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp"
+             "6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAAB"
+             "AncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0R"
+             "FRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tr"
+             "e4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2wBDAAQDAwMDAgQDAwMEBAQFBgoGBg"
+             "UFBgwICQcKDgwPDg4MDQ0PERYTDxAVEQ0NExoTFRcYGRkZDxIbHRsYHRYYGRj/2wBDAQQEBAYFBgsGBgsYEA0"
+             "QGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBj/3QAEAAH/2gAMAwEA"
+             "AhEDEQA/AMWiiivzk/qo/9k="
+                            options:0];
+  }
+  return data;
+}
+
++ (NSData*)PNGTestData {
+  NSBundle* bundle = [NSBundle bundleForClass:self];
+  NSURL* url = [bundle URLForResource:@"pngImage" withExtension:@"png"];
+  NSData* data = [NSData dataWithContentsOfURL:url];
+  if (!data.length) {
+    // When the tests are run outside the example project (podspec lint) the image may not be
+    // embedded in the test bundle. Fall back to the base64 string representation of the png.
+    data = [[NSData alloc]
+        initWithBase64EncodedString:
+            @"iVBORw0KGgoAAAAEQ2dCSVAAIAYsuHdmAAAADUlIRFIAAAAMAAAABwgGAAAAPLKsJAAAAARnQU1BAACxjwv8Y"
+             "QUAAAABc1JHQgCuzhzpAAAAIGNIUk0AAHomAACAhAAA+"
+             "gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAJcEh"
+             "ZcwAABxMAAAcTAc4gDwgAAAAOSURBVGMwdX71nxTMMKqBCAwAsfuEYQAAAABJRU5ErkJggg=="
+                            options:0];
+  }
+  return data;
+}
+
++ (NSData*)GIFTestData {
+  NSBundle* bundle = [NSBundle bundleForClass:self];
+  NSURL* url = [bundle URLForResource:@"gifImage" withExtension:@"gif"];
+  NSData* data = [NSData dataWithContentsOfURL:url];
+  if (!data.length) {
+    // When the tests are run outside the example project (podspec lint) the image may not be
+    // embedded in the test bundle. Fall back to the base64 string representation of the gif.
+    data = [[NSData alloc]
+        initWithBase64EncodedString:
+            @"R0lGODlhDAAHAPAAAOpCNQAAACH5BABkAAAAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAADAAHAAACCISP"
+             "qcvtD1UBACH5BABkAAAALAAAAAAMAAcAhuc/JPA/K+49Ne4+PvA7MrhYHoB+A4N9BYh+BYZ+E4xyG496HZJ"
+             "8F5J4GaRtE6tsH7tWIr9SK7xVKJl3IKpvI7lrKc1FLc5PLNJILsdTJMFVJsZWJshWIM9XIshWJNBWLd1SK9"
+             "BUMNFRNOlAI+9CMuNJMetHPnuCAF66F1u8FVu7GV27HGytG3utGH6rHGK1G3WxFWeuIHqlIG60IGi4JTnTDz"
+             "jZDy/VEy/eFTnVEDzXFxflABfjBRPmBRbnBxPrABvpARntAxLuCBXuCQTyAAb1BgvwACnmDSPpDSLjECPpED"
+             "HhFFDLGIeAFoiBFoqCF4uCHYWnHJGVJqSNJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+             "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+             "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdWgAIXCjE3PTtAPDUuByQfCzQ4Qj9BPjktBgAcC"
+             "StJRURGQzYwJyMdDDM6SkhHS0xRCAEgD1IsKikoLzJTDgQlEBQNT05NUBMVBQMmGCEZHhsaEhEiFoEAIfkEAG"
+             "QAAAAsAAAAAAwABwCFB+8ACewACu0ACe4ACO8AC+4ACu8ADOwAD+wAEOYAEekAA/EABfAAB/IAAfUAA/UAAP"
+             "cAAfcAAvYAA/cBBPQABfUABvQAB/UBBvYBCfAACPEAC/AACvIACvMBAPgAAPkAAPgBAPkBAvgBAPoAAPoBA"
+             "PsBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+             "AAAAAAAAAAAAAAAAAAAAAAAABkfAAadjeUxEEYnk8QBoLhUHCASJJCWLyiTiIZFG3lAoO4F4SiUwScywYCQQ8"
+             "ScEEokCG06D8pA4mBUWCQoIBwIGGQQGBgUFQQA7"
+                            options:0];
+  }
+  return data;
+}
+
+@end

+ 41 - 0
ios/Tests/ImageUtilTests.m

@@ -0,0 +1,41 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ImagePickerTestImages.h"
+
+@import image_picker;
+@import XCTest;
+
+@interface ImageUtilTests : XCTestCase
+@end
+
+@implementation ImageUtilTests
+
+- (void)testScaledImage_ShouldBeScaled {
+  UIImage *image = [UIImage imageWithData:ImagePickerTestImages.JPGTestData];
+  UIImage *newImage = [FLTImagePickerImageUtil scaledImage:image maxWidth:@3 maxHeight:@2];
+
+  XCTAssertEqual(newImage.size.width, 3);
+  XCTAssertEqual(newImage.size.height, 2);
+}
+
+- (void)testScaledGIFImage_ShouldBeScaled {
+  // gif image that frame size is 3 and the duration is 1 second.
+  GIFInfo *info = [FLTImagePickerImageUtil scaledGIFImage:ImagePickerTestImages.GIFTestData
+                                                 maxWidth:@3
+                                                maxHeight:@2];
+
+  NSArray<UIImage *> *images = info.images;
+  NSTimeInterval duration = info.interval;
+
+  XCTAssertEqual(images.count, 3);
+  XCTAssertEqual(duration, 1);
+
+  for (UIImage *newImage in images) {
+    XCTAssertEqual(newImage.size.width, 3);
+    XCTAssertEqual(newImage.size.height, 2);
+  }
+}
+
+@end

+ 89 - 0
ios/Tests/MetaDataUtilTests.m

@@ -0,0 +1,89 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ImagePickerTestImages.h"
+
+@import image_picker;
+@import XCTest;
+
+@interface MetaDataUtilTests : XCTestCase
+@end
+
+@implementation MetaDataUtilTests
+
+- (void)testGetImageMIMETypeFromImageData {
+  // test jpeg
+  XCTAssertEqual(
+      [FLTImagePickerMetaDataUtil getImageMIMETypeFromImageData:ImagePickerTestImages.JPGTestData],
+      FLTImagePickerMIMETypeJPEG);
+
+  // test png
+  XCTAssertEqual(
+      [FLTImagePickerMetaDataUtil getImageMIMETypeFromImageData:ImagePickerTestImages.PNGTestData],
+      FLTImagePickerMIMETypePNG);
+
+  // test gif
+  XCTAssertEqual(
+      [FLTImagePickerMetaDataUtil getImageMIMETypeFromImageData:ImagePickerTestImages.GIFTestData],
+      FLTImagePickerMIMETypeGIF);
+}
+
+- (void)testSuffixFromType {
+  // test jpeg
+  XCTAssertEqualObjects(
+      [FLTImagePickerMetaDataUtil imageTypeSuffixFromType:FLTImagePickerMIMETypeJPEG], @".jpg");
+
+  // test png
+  XCTAssertEqualObjects(
+      [FLTImagePickerMetaDataUtil imageTypeSuffixFromType:FLTImagePickerMIMETypePNG], @".png");
+
+  // test gif
+  XCTAssertEqualObjects(
+      [FLTImagePickerMetaDataUtil imageTypeSuffixFromType:FLTImagePickerMIMETypeGIF], @".gif");
+
+  // test other
+  XCTAssertNil([FLTImagePickerMetaDataUtil imageTypeSuffixFromType:FLTImagePickerMIMETypeOther]);
+}
+
+- (void)testGetMetaData {
+  NSDictionary *metaData =
+      [FLTImagePickerMetaDataUtil getMetaDataFromImageData:ImagePickerTestImages.JPGTestData];
+  NSDictionary *exif = [metaData objectForKey:(__bridge NSString *)kCGImagePropertyExifDictionary];
+  XCTAssertEqual([exif[(__bridge NSString *)kCGImagePropertyExifPixelXDimension] integerValue], 12);
+}
+
+- (void)testWriteMetaData {
+  NSData *dataJPG = ImagePickerTestImages.JPGTestData;
+
+  NSDictionary *metaData = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataJPG];
+  NSString *tmpFile = [NSString stringWithFormat:@"image_picker_test.jpg"];
+  NSString *tmpDirectory = NSTemporaryDirectory();
+  NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile];
+  NSData *newData = [FLTImagePickerMetaDataUtil updateMetaData:metaData toImage:dataJPG];
+  if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:newData attributes:nil]) {
+    NSData *savedTmpImageData = [NSData dataWithContentsOfFile:tmpPath];
+    NSDictionary *tmpMetaData =
+        [FLTImagePickerMetaDataUtil getMetaDataFromImageData:savedTmpImageData];
+    XCTAssert([tmpMetaData isEqualToDictionary:metaData]);
+  } else {
+    XCTAssert(NO);
+  }
+}
+
+- (void)testConvertImageToData {
+  UIImage *imageJPG = [UIImage imageWithData:ImagePickerTestImages.JPGTestData];
+  NSData *convertedDataJPG = [FLTImagePickerMetaDataUtil convertImage:imageJPG
+                                                            usingType:FLTImagePickerMIMETypeJPEG
+                                                              quality:@(0.5)];
+  XCTAssertEqual([FLTImagePickerMetaDataUtil getImageMIMETypeFromImageData:convertedDataJPG],
+                 FLTImagePickerMIMETypeJPEG);
+
+  NSData *convertedDataPNG = [FLTImagePickerMetaDataUtil convertImage:imageJPG
+                                                            usingType:FLTImagePickerMIMETypePNG
+                                                              quality:nil];
+  XCTAssertEqual([FLTImagePickerMetaDataUtil getImageMIMETypeFromImageData:convertedDataPNG],
+                 FLTImagePickerMIMETypePNG);
+}
+
+@end

+ 137 - 0
ios/Tests/PhotoAssetUtilTests.m

@@ -0,0 +1,137 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ImagePickerTestImages.h"
+
+@import image_picker;
+@import XCTest;
+
+@interface PhotoAssetUtilTests : XCTestCase
+@end
+
+@implementation PhotoAssetUtilTests
+
+- (void)getAssetFromImagePickerInfoShouldReturnNilIfNotAvailable {
+  NSDictionary *mockData = @{};
+  XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:mockData]);
+}
+
+- (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndMetaData {
+  // test jpg
+  NSData *dataJPG = ImagePickerTestImages.JPGTestData;
+  UIImage *imageJPG = [UIImage imageWithData:dataJPG];
+  NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataJPG
+                                                                                  image:imageJPG
+                                                                               maxWidth:nil
+                                                                              maxHeight:nil
+                                                                           imageQuality:nil];
+  XCTAssertNotNil(savedPathJPG);
+  XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4], @".jpg");
+
+  NSDictionary *originalMetaDataJPG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataJPG];
+  NSData *newDataJPG = [NSData dataWithContentsOfFile:savedPathJPG];
+  NSDictionary *newMetaDataJPG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:newDataJPG];
+  XCTAssertEqualObjects(originalMetaDataJPG[@"ProfileName"], newMetaDataJPG[@"ProfileName"]);
+
+  // test png
+  NSData *dataPNG = ImagePickerTestImages.PNGTestData;
+  UIImage *imagePNG = [UIImage imageWithData:dataPNG];
+  NSString *savedPathPNG = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataPNG
+                                                                                  image:imagePNG
+                                                                               maxWidth:nil
+                                                                              maxHeight:nil
+                                                                           imageQuality:nil];
+  XCTAssertNotNil(savedPathPNG);
+  XCTAssertEqualObjects([savedPathPNG substringFromIndex:savedPathPNG.length - 4], @".png");
+
+  NSDictionary *originalMetaDataPNG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataPNG];
+  NSData *newDataPNG = [NSData dataWithContentsOfFile:savedPathPNG];
+  NSDictionary *newMetaDataPNG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:newDataPNG];
+  XCTAssertEqualObjects(originalMetaDataPNG[@"ProfileName"], newMetaDataPNG[@"ProfileName"]);
+}
+
+- (void)testSaveImageWithPickerInfo_ShouldSaveWithDefaultExtention {
+  UIImage *imageJPG = [UIImage imageWithData:ImagePickerTestImages.JPGTestData];
+  NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil
+                                                                           image:imageJPG
+                                                                    imageQuality:nil];
+
+  XCTAssertNotNil(savedPathJPG);
+  // should be saved as
+  XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4],
+                        kFLTImagePickerDefaultSuffix);
+}
+
+- (void)testSaveImageWithPickerInfo_ShouldSaveWithTheCorrectExtentionAndMetaData {
+  NSDictionary *dummyInfo = @{
+    UIImagePickerControllerMediaMetadata : @{
+      (__bridge NSString *)kCGImagePropertyExifDictionary :
+          @{(__bridge NSString *)kCGImagePropertyExifMakerNote : @"aNote"}
+    }
+  };
+  UIImage *imageJPG = [UIImage imageWithData:ImagePickerTestImages.JPGTestData];
+  NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:dummyInfo
+                                                                           image:imageJPG
+                                                                    imageQuality:nil];
+  NSData *data = [NSData dataWithContentsOfFile:savedPathJPG];
+  NSDictionary *meta = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:data];
+  XCTAssertEqualObjects(meta[(__bridge NSString *)kCGImagePropertyExifDictionary]
+                            [(__bridge NSString *)kCGImagePropertyExifMakerNote],
+                        @"aNote");
+}
+
+- (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation {
+  // test gif
+  NSData *dataGIF = ImagePickerTestImages.GIFTestData;
+  UIImage *imageGIF = [UIImage imageWithData:dataGIF];
+  CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil);
+
+  size_t numberOfFrames = CGImageSourceGetCount(imageSource);
+
+  NSNumber *nilSize = (NSNumber *)[NSNull null];
+  NSString *savedPathGIF = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataGIF
+                                                                                  image:imageGIF
+                                                                               maxWidth:nilSize
+                                                                              maxHeight:nilSize
+                                                                           imageQuality:nil];
+  XCTAssertNotNil(savedPathGIF);
+  XCTAssertEqualObjects([savedPathGIF substringFromIndex:savedPathGIF.length - 4], @".gif");
+
+  NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPathGIF];
+
+  CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil);
+
+  size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource);
+
+  XCTAssertEqual(numberOfFrames, newNumberOfFrames);
+}
+
+- (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation {
+  // test gif
+  NSData *dataGIF = ImagePickerTestImages.GIFTestData;
+  UIImage *imageGIF = [UIImage imageWithData:dataGIF];
+
+  CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil);
+
+  size_t numberOfFrames = CGImageSourceGetCount(imageSource);
+
+  NSString *savedPathGIF = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataGIF
+                                                                                  image:imageGIF
+                                                                               maxWidth:@3
+                                                                              maxHeight:@2
+                                                                           imageQuality:nil];
+  NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPathGIF];
+  UIImage *newImage = [[UIImage alloc] initWithData:newDataGIF];
+
+  XCTAssertEqual(newImage.size.width, 3);
+  XCTAssertEqual(newImage.size.height, 2);
+
+  CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil);
+
+  size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource);
+
+  XCTAssertEqual(numberOfFrames, newNumberOfFrames);
+}
+
+@end

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott