├── .circleci └── config.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── build.gradle ├── gradle.properties ├── image_picker_saver.iml ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── flutter │ │ └── plugins │ │ └── imagepickersaver │ │ ├── CapturePhotoUtils.java │ │ ├── ExifDataCopier.java │ │ ├── FileUtils.java │ │ ├── ImagePickerDelegate.java │ │ ├── ImagePickerFileProvider.java │ │ ├── ImagePickerSaverPlugin.java │ │ └── ImageResizer.java │ └── res │ └── xml │ └── flutter_image_picker_file_paths.xml ├── example ├── .gitignore ├── .metadata ├── README.md ├── android.iml ├── android │ ├── .gitignore │ ├── .idea │ │ └── libraries │ │ │ ├── Gradle__androidx_annotation_annotation_1_0_0_jar.xml │ │ │ ├── Gradle__androidx_arch_core_core_common_2_0_0_jar.xml │ │ │ ├── Gradle__androidx_collection_collection_1_0_0_jar.xml │ │ │ ├── Gradle__androidx_core_core_1_0_0_aar.xml │ │ │ ├── Gradle__androidx_lifecycle_lifecycle_common_2_0_0_jar.xml │ │ │ ├── Gradle__androidx_lifecycle_lifecycle_runtime_2_0_0_aar.xml │ │ │ ├── Gradle__androidx_test_espresso_espresso_core_3_1_0_aar.xml │ │ │ ├── Gradle__androidx_test_espresso_espresso_idling_resource_3_1_0_aar.xml │ │ │ ├── Gradle__androidx_test_monitor_1_1_0_aar.xml │ │ │ ├── Gradle__androidx_test_runner_1_1_0_aar.xml │ │ │ ├── Gradle__androidx_versionedparcelable_versionedparcelable_1_0_0_aar.xml │ │ │ ├── Gradle__com_google_code_findbugs_jsr305_2_0_1_jar.xml │ │ │ ├── Gradle__com_squareup_javawriter_2_1_1_jar.xml │ │ │ ├── Gradle__javax_inject_javax_inject_1_jar.xml │ │ │ ├── Gradle__junit_junit_4_12_jar.xml │ │ │ ├── Gradle__net_bytebuddy_byte_buddy_1_8_0_jar.xml │ │ │ ├── Gradle__net_bytebuddy_byte_buddy_agent_1_8_0_jar.xml │ │ │ ├── Gradle__net_sf_kxml_kxml2_2_3_0_jar.xml │ │ │ ├── Gradle__org_hamcrest_hamcrest_core_1_3_jar.xml │ │ │ ├── Gradle__org_hamcrest_hamcrest_integration_1_3_jar.xml │ │ │ ├── Gradle__org_hamcrest_hamcrest_library_1_3_jar.xml │ │ │ ├── Gradle__org_mockito_mockito_core_2_17_0_jar.xml │ │ │ └── Gradle__org_objenesis_objenesis_2_6_jar.xml │ ├── app │ │ ├── app.iml │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── flutter │ │ │ │ │ └── plugins │ │ │ │ │ ├── GeneratedPluginRegistrant.java │ │ │ │ │ └── imagepickersaverexample │ │ │ │ │ └── MainActivity.java │ │ │ ├── kotlin │ │ │ │ └── create │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ ├── profile │ │ │ └── AndroidManifest.xml │ │ │ └── test │ │ │ ├── java │ │ │ └── io │ │ │ │ └── flutter │ │ │ │ └── plugins │ │ │ │ └── imagepicker │ │ │ │ ├── ImagePickerDelegateTest.java │ │ │ │ └── ImagePickerPluginTest.java │ │ │ └── resources │ │ │ └── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── image_picker_example.iml ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Generated.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Pods │ │ └── Local Podspecs │ │ │ └── Flutter.podspec.json │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner │ │ ├── .gitignore │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── Runner-Bridging-Header.h │ │ └── main.m │ └── ServiceDefinitions.json ├── lib │ ├── main.dart │ └── mian_screenhot.dart ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ └── index.html ├── gradlew ├── gradlew.bat ├── ios ├── Classes │ ├── ImagePickerSaverPlugin.h │ └── ImagePickerSaverPlugin.m └── image_picker_saver.podspec ├── lib └── image_picker_saver.dart ├── pubspec.yaml └── test └── image_picker_test.dart /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cirrusci/flutter 6 | steps: 7 | - checkout 8 | - run: flutter doctor 9 | - run: flutter test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \..* 2 | example/build 3 | *.lock 4 | *. 5 | *. 6 | .flutter-plugins 7 | .packages 8 | .gitkeep 9 | example/ios/.symlinks 10 | UserInterfaceState.xcuserstate 11 | example/ios/Runner.xcworkspace/xcshareddata 12 | example/ios/Runner.xcworkspace/xcuserdata 13 | example/ios/Flutter/App.framework 14 | example/ios/Flutter/Flutter.framework 15 | example/ios/Flutter/flutter_assets 16 | example/ios/Pods 17 | example/android/gradle/* 18 | 19 | # Miscellaneous 20 | *.class 21 | *.lock 22 | *.log 23 | *.pyc 24 | *.swp 25 | .DS_Store 26 | .atom/ 27 | .buildlog/ 28 | .history 29 | .svn/ 30 | 31 | # IntelliJ related 32 | *.iml 33 | *.ipr 34 | *.iws 35 | .idea/ 36 | 37 | # Visual Studio Code related 38 | .vscode/ 39 | 40 | # Flutter/Dart/Pub related 41 | **/doc/api/ 42 | .dart_tool/ 43 | .flutter-plugins 44 | .packages 45 | .pub-cache/ 46 | .pub/ 47 | build/ 48 | 49 | # Android related 50 | **/android/**/gradle-wrapper.jar 51 | **/android/.gradle 52 | **/android/captures/ 53 | **/android/gradlew 54 | **/android/gradlew.bat 55 | **/android/local.properties 56 | **/android/**/GeneratedPluginRegistrant.java 57 | android/gradle/* 58 | 59 | # iOS/XCode related 60 | **/ios/**/*.mode1v3 61 | **/ios/**/*.mode2v3 62 | **/ios/**/*.moved-aside 63 | **/ios/**/*.pbxuser 64 | **/ios/**/*.perspectivev3 65 | **/ios/**/*sync/ 66 | **/ios/**/.sconsign.dblite 67 | **/ios/**/.tags* 68 | **/ios/**/.vagrant/ 69 | **/ios/**/DerivedData/ 70 | **/ios/**/Icon? 71 | **/ios/**/Pods/ 72 | **/ios/**/.symlinks/ 73 | **/ios/**/profile 74 | **/ios/**/xcuserdata 75 | **/ios/.generated/ 76 | **/ios/Flutter/App.framework 77 | **/ios/Flutter/Flutter.framework 78 | **/ios/Flutter/Generated.xcconfig 79 | **/ios/Flutter/app.flx 80 | **/ios/Flutter/app.zip 81 | **/ios/Flutter/flutter_assets/ 82 | **/ios/ServiceDefinitions.json 83 | **/ios/Runner/GeneratedPluginRegistrant.* 84 | 85 | # Exceptions to above rules. 86 | !**/ios/**/default.mode1v3 87 | !**/ios/**/default.mode2v3 88 | !**/ios/**/default.pbxuser 89 | !**/ios/**/default.perspectivev3 90 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 91 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart: 3 | - stable 4 | os: 5 | - linux 6 | sudo: false 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test # you need this source to get the right version of libstdc++6 11 | packages: 12 | - libstdc++6 13 | - fonts-droid 14 | install: 15 | - echo 'Avoid default Travis CI install step' # this is to avoid an error with pub in Travis 16 | before_script: 17 | - cd .. 18 | - git clone https://github.com/flutter/flutter.git 19 | - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH 20 | - flutter doctor 21 | script: 22 | - cd $TRAVIS_BUILD_DIR 23 | - flutter packages get 24 | - flutter analyze --no-pub --no-current-package lib 25 | - flutter test -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 2 | * **Breaking change** Migrate to AndroidX on android support library. 3 | If you are using the android support library and you want to use that version, you should migrate to Androidx your project. 4 | 5 | ## 0.2.0 6 | * Added title and description as a parameter 7 | 8 | ## 0.1.0 9 | * Add return saved file path 10 | * Add save screenshot sample 11 | 12 | ## 0.0.2 13 | * Android implemented,iOS implemented. 14 | * Removed unused parameters. 15 | 16 | 17 | ## 0.0.1 18 | forked from official plugin image_picker 19 | and add saver function to save image to photo gallery. 20 | Android implemented,iOS in developing 21 | 22 | 23 | 24 | ---- The following is the official plugin description --- 25 | ## 0.4.6 26 | 27 | * Added support for picking remote images. 28 | 29 | ## 0.4.5 30 | 31 | * Bugfixes, code cleanup, more test coverage. 32 | 33 | ## 0.4.4 34 | 35 | * Updated Gradle tooling to match Android Studio 3.1.2. 36 | 37 | ## 0.4.3 38 | 39 | * Bugfix: on iOS the `pickVideo` method will now return null when the user cancels picking a video. 40 | 41 | ## 0.4.2 42 | 43 | * Added support for picking videos. 44 | * Updated example app to show video preview. 45 | 46 | ## 0.4.1 47 | 48 | * Bugfix: the `pickImage` method will now return null when the user cancels picking the image, instead of hanging indefinitely. 49 | * Removed the third party library dependency for taking pictures with the camera. 50 | 51 | ## 0.4.0 52 | 53 | * **Breaking change**. The `source` parameter for the `pickImage` is now required. Also, the `ImageSource.any` option doesn't exist anymore. 54 | * Use the native Android image gallery for picking images instead of a custom UI. 55 | 56 | ## 0.3.1 57 | 58 | * Bugfix: Android version correctly asks for runtime camera permission when using `ImageSource.camera`. 59 | 60 | ## 0.3.0 61 | 62 | * **Breaking change**. Set SDK constraints to match the Flutter beta release. 63 | 64 | ## 0.2.1 65 | 66 | * Simplified and upgraded Android project template to Android SDK 27. 67 | * Updated package description. 68 | 69 | ## 0.2.0 70 | 71 | * **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin 72 | 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in 73 | order to use this version of the plugin. Instructions can be found 74 | [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). 75 | 76 | ## 0.1.5 77 | 78 | * Added FLT prefix to iOS types 79 | 80 | ## 0.1.4 81 | 82 | * Bugfix: canceling image picking threw exception. 83 | * Bugfix: errors in plugin state management. 84 | 85 | ## 0.1.3 86 | 87 | * Added optional source argument to pickImage for controlling where the image comes from. 88 | 89 | ## 0.1.2 90 | 91 | * Added optional maxWidth and maxHeight arguments to pickImage. 92 | 93 | ## 0.1.1 94 | 95 | * Updated Gradle repositories declaration to avoid the need for manual configuration 96 | in the consuming app. 97 | 98 | ## 0.1.0+1 99 | 100 | * Updated readme and description in pubspec.yaml 101 | 102 | ## 0.1.0 103 | 104 | * Updated dependencies 105 | * **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: 106 | ```gradle 107 | allprojects { 108 | repositories { 109 | jcenter() 110 | maven { // NEW 111 | url "https://maven.google.com" // NEW 112 | } // NEW 113 | } 114 | } 115 | ``` 116 | 117 | ## 0.0.3 118 | 119 | * Fix for crash on iPad when showing the Camera/Gallery selection dialog 120 | 121 | ## 0.0.2+2 122 | 123 | * Updated README 124 | 125 | ## 0.0.2+1 126 | 127 | * Updated README 128 | 129 | ## 0.0.2 130 | 131 | * Fix crash when trying to access camera on a device without camera (e.g. the Simulator) 132 | 133 | ## 0.0.1 134 | 135 | * Initial Release 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017, the Flutter project authors. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- 28 | 29 | Apache License 30 | Version 2.0, January 2004 31 | http://www.apache.org/licenses/ 32 | 33 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 34 | 35 | 1. Definitions. 36 | 37 | "License" shall mean the terms and conditions for use, reproduction, 38 | and distribution as defined by Sections 1 through 9 of this document. 39 | 40 | "Licensor" shall mean the copyright owner or entity authorized by 41 | the copyright owner that is granting the License. 42 | 43 | "Legal Entity" shall mean the union of the acting entity and all 44 | other entities that control, are controlled by, or are under common 45 | control with that entity. For the purposes of this definition, 46 | "control" means (i) the power, direct or indirect, to cause the 47 | direction or management of such entity, whether by contract or 48 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 49 | outstanding shares, or (iii) beneficial ownership of such entity. 50 | 51 | "You" (or "Your") shall mean an individual or Legal Entity 52 | exercising permissions granted by this License. 53 | 54 | "Source" form shall mean the preferred form for making modifications, 55 | including but not limited to software source code, documentation 56 | source, and configuration files. 57 | 58 | "Object" form shall mean any form resulting from mechanical 59 | transformation or translation of a Source form, including but 60 | not limited to compiled object code, generated documentation, 61 | and conversions to other media types. 62 | 63 | "Work" shall mean the work of authorship, whether in Source or 64 | Object form, made available under the License, as indicated by a 65 | copyright notice that is included in or attached to the work 66 | (an example is provided in the Appendix below). 67 | 68 | "Derivative Works" shall mean any work, whether in Source or Object 69 | form, that is based on (or derived from) the Work and for which the 70 | editorial revisions, annotations, elaborations, or other modifications 71 | represent, as a whole, an original work of authorship. For the purposes 72 | of this License, Derivative Works shall not include works that remain 73 | separable from, or merely link (or bind by name) to the interfaces of, 74 | the Work and Derivative Works thereof. 75 | 76 | "Contribution" shall mean any work of authorship, including 77 | the original version of the Work and any modifications or additions 78 | to that Work or Derivative Works thereof, that is intentionally 79 | submitted to Licensor for inclusion in the Work by the copyright owner 80 | or by an individual or Legal Entity authorized to submit on behalf of 81 | the copyright owner. For the purposes of this definition, "submitted" 82 | means any form of electronic, verbal, or written communication sent 83 | to the Licensor or its representatives, including but not limited to 84 | communication on electronic mailing lists, source code control systems, 85 | and issue tracking systems that are managed by, or on behalf of, the 86 | Licensor for the purpose of discussing and improving the Work, but 87 | excluding communication that is conspicuously marked or otherwise 88 | designated in writing by the copyright owner as "Not a Contribution." 89 | 90 | "Contributor" shall mean Licensor and any individual or Legal Entity 91 | on behalf of whom a Contribution has been received by Licensor and 92 | subsequently incorporated within the Work. 93 | 94 | 2. Grant of Copyright License. Subject to the terms and conditions of 95 | this License, each Contributor hereby grants to You a perpetual, 96 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 97 | copyright license to reproduce, prepare Derivative Works of, 98 | publicly display, publicly perform, sublicense, and distribute the 99 | Work and such Derivative Works in Source or Object form. 100 | 101 | 3. Grant of Patent License. Subject to the terms and conditions of 102 | this License, each Contributor hereby grants to You a perpetual, 103 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 104 | (except as stated in this section) patent license to make, have made, 105 | use, offer to sell, sell, import, and otherwise transfer the Work, 106 | where such license applies only to those patent claims licensable 107 | by such Contributor that are necessarily infringed by their 108 | Contribution(s) alone or by combination of their Contribution(s) 109 | with the Work to which such Contribution(s) was submitted. If You 110 | institute patent litigation against any entity (including a 111 | cross-claim or counterclaim in a lawsuit) alleging that the Work 112 | or a Contribution incorporated within the Work constitutes direct 113 | or contributory patent infringement, then any patent licenses 114 | granted to You under this License for that Work shall terminate 115 | as of the date such litigation is filed. 116 | 117 | 4. Redistribution. You may reproduce and distribute copies of the 118 | Work or Derivative Works thereof in any medium, with or without 119 | modifications, and in Source or Object form, provided that You 120 | meet the following conditions: 121 | 122 | (a) You must give any other recipients of the Work or 123 | Derivative Works a copy of this License; and 124 | 125 | (b) You must cause any modified files to carry prominent notices 126 | stating that You changed the files; and 127 | 128 | (c) You must retain, in the Source form of any Derivative Works 129 | that You distribute, all copyright, patent, trademark, and 130 | attribution notices from the Source form of the Work, 131 | excluding those notices that do not pertain to any part of 132 | the Derivative Works; and 133 | 134 | (d) If the Work includes a "NOTICE" text file as part of its 135 | distribution, then any Derivative Works that You distribute must 136 | include a readable copy of the attribution notices contained 137 | within such NOTICE file, excluding those notices that do not 138 | pertain to any part of the Derivative Works, in at least one 139 | of the following places: within a NOTICE text file distributed 140 | as part of the Derivative Works; within the Source form or 141 | documentation, if provided along with the Derivative Works; or, 142 | within a display generated by the Derivative Works, if and 143 | wherever such third-party notices normally appear. The contents 144 | of the NOTICE file are for informational purposes only and 145 | do not modify the License. You may add Your own attribution 146 | notices within Derivative Works that You distribute, alongside 147 | or as an addendum to the NOTICE text from the Work, provided 148 | that such additional attribution notices cannot be construed 149 | as modifying the License. 150 | 151 | You may add Your own copyright statement to Your modifications and 152 | may provide additional or different license terms and conditions 153 | for use, reproduction, or distribution of Your modifications, or 154 | for any such Derivative Works as a whole, provided Your use, 155 | reproduction, and distribution of the Work otherwise complies with 156 | the conditions stated in this License. 157 | 158 | 5. Submission of Contributions. Unless You explicitly state otherwise, 159 | any Contribution intentionally submitted for inclusion in the Work 160 | by You to the Licensor shall be under the terms and conditions of 161 | this License, without any additional terms or conditions. 162 | Notwithstanding the above, nothing herein shall supersede or modify 163 | the terms of any separate license agreement you may have executed 164 | with Licensor regarding such Contributions. 165 | 166 | 6. Trademarks. This License does not grant permission to use the trade 167 | names, trademarks, service marks, or product names of the Licensor, 168 | except as required for reasonable and customary use in describing the 169 | origin of the Work and reproducing the content of the NOTICE file. 170 | 171 | 7. Disclaimer of Warranty. Unless required by applicable law or 172 | agreed to in writing, Licensor provides the Work (and each 173 | Contributor provides its Contributions) on an "AS IS" BASIS, 174 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 175 | implied, including, without limitation, any warranties or conditions 176 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 177 | PARTICULAR PURPOSE. You are solely responsible for determining the 178 | appropriateness of using or redistributing the Work and assume any 179 | risks associated with Your exercise of permissions under this License. 180 | 181 | 8. Limitation of Liability. In no event and under no legal theory, 182 | whether in tort (including negligence), contract, or otherwise, 183 | unless required by applicable law (such as deliberate and grossly 184 | negligent acts) or agreed to in writing, shall any Contributor be 185 | liable to You for damages, including any direct, indirect, special, 186 | incidental, or consequential damages of any character arising as a 187 | result of this License or out of the use or inability to use the 188 | Work (including but not limited to damages for loss of goodwill, 189 | work stoppage, computer failure or malfunction, or any and all 190 | other commercial damages or losses), even if such Contributor 191 | has been advised of the possibility of such damages. 192 | 193 | 9. Accepting Warranty or Additional Liability. While redistributing 194 | the Work or Derivative Works thereof, You may choose to offer, 195 | and charge a fee for, acceptance of support, warranty, indemnity, 196 | or other liability obligations and/or rights consistent with this 197 | License. However, in accepting such obligations, You may act only 198 | on Your own behalf and on Your sole responsibility, not on behalf 199 | of any other Contributor, and only if You agree to indemnify, 200 | defend, and hold each Contributor harmless for any liability 201 | incurred by, or claims asserted against, such Contributor by reason 202 | of your accepting any such warranty or additional liability. 203 | 204 | END OF TERMS AND CONDITIONS 205 | 206 | APPENDIX: How to apply the Apache License to your work. 207 | 208 | To apply the Apache License to your work, attach the following 209 | boilerplate notice, with the fields enclosed by brackets "[]" 210 | replaced with your own identifying information. (Don't include 211 | the brackets!) The text should be enclosed in the appropriate 212 | comment syntax for the file format. We also recommend that a 213 | file or class name and description of purpose be included on the 214 | same "printed page" as the copyright notice for easier 215 | identification within third-party archives. 216 | 217 | Copyright 2011 - 2013 Paul Burke 218 | 219 | Licensed under the Apache License, Version 2.0 (the "License"); 220 | you may not use this file except in compliance with the License. 221 | You may obtain a copy of the License at 222 | 223 | http://www.apache.org/licenses/LICENSE-2.0 224 | 225 | Unless required by applicable law or agreed to in writing, software 226 | distributed under the License is distributed on an "AS IS" BASIS, 227 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 228 | See the License for the specific language governing permissions and 229 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Picker and Saver plugin for Flutter 2 | [![pub package](https://img.shields.io/pub/v/image_picker_saver.svg)](https://pub.dartlang.org/packages/image_picker_saver) 3 | 4 | Android supported 5 | 6 | IOS supported 8.0+ 7 | 8 | forked from official plugin image_picker and add saver function to save image to photo gallery. 9 | 10 | ## Installation 11 | click the pub version icon to read hwo to install this plugin. 12 | 13 | 14 | ### Save image Example 15 | ``` dart 16 | 17 | void _onImageSaveButtonPressed() async { 18 | print("_onImageSaveButtonPressed"); 19 | var response = await http 20 | .get('http://upload.art.ifeng.com/2017/0425/1493105660290.jpg'); 21 | 22 | debugPrint(response.statusCode.toString()); 23 | 24 | var filePath = await ImagePickerSaver.saveFile( 25 | fileData: response.bodyBytes); 26 | 27 | var savedFile= File.fromUri(Uri.file(filePath)); 28 | setState(() { 29 | _imageFile = Future.sync(() => savedFile); 30 | }); 31 | } 32 | 33 | ``` 34 | 35 | #---- The following is the official plugin description --- 36 | 37 | # Image Picker plugin for Flutter 38 | 39 | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dartlang.org/packages/image_picker) 40 | 41 | A Flutter plugin for iOS and Android for picking images from the image library, 42 | and taking new pictures with the camera. 43 | 44 | *Note*: This plugin is still under development, and some APIs might not be available yet. [Feedback welcome](https://github.com/flutter/flutter/issues) and [Pull Requests](https://github.com/flutter/plugins/pulls) are most welcome! 45 | 46 | ## Installation 47 | 48 | First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). 49 | 50 | ### iOS 51 | 52 | Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: 53 | 54 | * `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. 55 | * `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor. 56 | * `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. 57 | 58 | ### Android 59 | 60 | No configuration required - the plugin should work out of the box. 61 | 62 | ### Example 63 | 64 | ``` dart 65 | import 'package:image_picker/image_picker.dart'; 66 | 67 | class MyHomePage extends StatefulWidget { 68 | @override 69 | _MyHomePageState createState() => new _MyHomePageState(); 70 | } 71 | 72 | class _MyHomePageState extends State { 73 | File _image; 74 | 75 | Future getImage() async { 76 | var image = await ImagePicker.pickImage(source: ImageSource.camera); 77 | 78 | setState(() { 79 | _image = image; 80 | }); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return new Scaffold( 86 | appBar: new AppBar( 87 | title: new Text('Image Picker Example'), 88 | ), 89 | body: new Center( 90 | child: _image == null 91 | ? new Text('No image selected.') 92 | : new Image.file(_image), 93 | ), 94 | floatingActionButton: new FloatingActionButton( 95 | onPressed: getImage, 96 | tooltip: 'Pick Image', 97 | child: new Icon(Icons.add_a_photo), 98 | ), 99 | ); 100 | } 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.yourcompany.image_picker' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.2.0' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 28 26 | 27 | defaultConfig { 28 | minSdkVersion 16 29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 30 | } 31 | lintOptions { 32 | disable 'InvalidPackage' 33 | } 34 | } 35 | 36 | dependencies { 37 | api 'androidx.core:core:1.0.0' 38 | } 39 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/image_picker_saver.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'imagepickersaver' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/imagepickersaver/CapturePhotoUtils.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins.imagepickersaver; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.ContentUris; 5 | import android.content.ContentValues; 6 | import android.database.Cursor; 7 | import android.graphics.Bitmap; 8 | import android.graphics.Matrix; 9 | import android.net.Uri; 10 | import android.provider.MediaStore; 11 | import android.provider.MediaStore.Images; 12 | 13 | import java.io.BufferedInputStream; 14 | import java.io.ByteArrayInputStream; 15 | import java.io.FileNotFoundException; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | import java.net.URLConnection; 20 | 21 | /** 22 | * Android internals have been modified to store images in the media folder with 23 | * the correct date meta data 24 | * 25 | * @author samuelkirton 26 | */ 27 | public class CapturePhotoUtils { 28 | 29 | /** 30 | * A copy of the Android internals insertImage method, this method populates the 31 | * meta data with DATE_ADDED and DATE_TAKEN. This fixes a common problem where media 32 | * that is inserted manually gets saved at the end of the gallery (because date is not populated). 33 | * 34 | * @see android.provider.MediaStore.Images.Media#insertImage(ContentResolver, Bitmap, String, String) 35 | */ 36 | public static final String insertImage(ContentResolver cr, 37 | byte[] source, 38 | String title, 39 | String description) throws IOException { 40 | 41 | InputStream is = new BufferedInputStream(new ByteArrayInputStream(source)); 42 | String mimeType = URLConnection.guessContentTypeFromStream(is); 43 | 44 | ContentValues values = new ContentValues(); 45 | values.put(Images.Media.TITLE, title); 46 | values.put(Images.Media.DISPLAY_NAME, title); 47 | values.put(Images.Media.DESCRIPTION, description); 48 | values.put(Images.Media.MIME_TYPE, mimeType); 49 | // Add the date meta data to ensure the image is added at the front of the gallery 50 | values.put(Images.Media.DATE_ADDED, System.currentTimeMillis()); 51 | values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis()); 52 | 53 | Uri url = null; 54 | String stringUrl = ""; /* value to be returned */ 55 | 56 | try { 57 | url = cr.insert(Images.Media.EXTERNAL_CONTENT_URI, values); 58 | 59 | if (source != null) { 60 | OutputStream imageOut = cr.openOutputStream(url); 61 | try { 62 | //source.compress(Bitmap.CompressFormat.JPEG, 100, imageOut); 63 | imageOut.write(source); 64 | } finally { 65 | imageOut.close(); 66 | } 67 | 68 | long id = ContentUris.parseId(url); 69 | // Wait until MINI_KIND thumbnail is generated. 70 | Bitmap miniThumb = Images.Thumbnails.getThumbnail(cr, id, Images.Thumbnails.MINI_KIND, null); 71 | // This is for backward compatibility. 72 | storeThumbnail(cr, miniThumb, id, 50F, 50F, Images.Thumbnails.MICRO_KIND); 73 | 74 | 75 | } else { 76 | cr.delete(url, null, null); 77 | url = null; 78 | } 79 | } catch (Exception e) { 80 | if (url != null) { 81 | cr.delete(url, null, null); 82 | url = null; 83 | } 84 | } 85 | 86 | if (url != null) { 87 | stringUrl = getFilePathFromContentUri(url, cr); 88 | } 89 | 90 | 91 | return stringUrl; 92 | } 93 | 94 | /** 95 | * A copy of the Android internals StoreThumbnail method, it used with the insertImage to 96 | * populate the android.provider.MediaStore.Images.Media#insertImage with all the correct 97 | * meta data. The StoreThumbnail method is private so it must be duplicated here. 98 | * 99 | * @see android.provider.MediaStore.Images.Media (StoreThumbnail private method) 100 | */ 101 | private static final Bitmap storeThumbnail( 102 | ContentResolver cr, 103 | Bitmap source, 104 | long id, 105 | float width, 106 | float height, 107 | int kind) { 108 | 109 | // create the matrix to scale it 110 | Matrix matrix = new Matrix(); 111 | 112 | float scaleX = width / source.getWidth(); 113 | float scaleY = height / source.getHeight(); 114 | 115 | matrix.setScale(scaleX, scaleY); 116 | 117 | Bitmap thumb = Bitmap.createBitmap(source, 0, 0, 118 | source.getWidth(), 119 | source.getHeight(), matrix, 120 | true 121 | ); 122 | 123 | ContentValues values = new ContentValues(4); 124 | values.put(Images.Thumbnails.KIND, kind); 125 | values.put(Images.Thumbnails.IMAGE_ID, (int) id); 126 | values.put(Images.Thumbnails.HEIGHT, thumb.getHeight()); 127 | values.put(Images.Thumbnails.WIDTH, thumb.getWidth()); 128 | 129 | Uri url = cr.insert(Images.Thumbnails.EXTERNAL_CONTENT_URI, values); 130 | if (url == null) return null; 131 | 132 | try { 133 | OutputStream thumbOut = cr.openOutputStream(url); 134 | //thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut); 135 | thumbOut.close(); 136 | return thumb; 137 | } catch (FileNotFoundException ex) { 138 | return null; 139 | } catch (IOException ex) { 140 | return null; 141 | } 142 | } 143 | 144 | /** 145 | * Gets the corresponding path to a file from the given content:// URI 146 | * 147 | * @param selectedVideoUri The content:// URI to find the file path from 148 | * @param contentResolver The content resolver to use to perform the query. 149 | * @return the file path as a string 150 | */ 151 | public static String getFilePathFromContentUri(Uri selectedVideoUri, 152 | ContentResolver contentResolver) { 153 | String filePath; 154 | String[] filePathColumn = {MediaStore.MediaColumns.DATA}; 155 | 156 | Cursor cursor = contentResolver.query(selectedVideoUri, filePathColumn, null, null, null); 157 | // 也可用下面的方法拿到cursor 158 | // Cursor cursor = this.context.managedQuery(selectedVideoUri, filePathColumn, null, null, null); 159 | 160 | cursor.moveToFirst(); 161 | 162 | int columnIndex = cursor.getColumnIndex(filePathColumn[0]); 163 | filePath = cursor.getString(columnIndex); 164 | cursor.close(); 165 | return filePath; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/imagepickersaver/ExifDataCopier.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package io.flutter.plugins.imagepickersaver; 6 | 7 | import android.media.ExifInterface; 8 | import android.util.Log; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | class ExifDataCopier { 14 | void copyExif(String filePathOri, String filePathDest) { 15 | try { 16 | ExifInterface oldExif = new ExifInterface(filePathOri); 17 | ExifInterface newExif = new ExifInterface(filePathDest); 18 | 19 | List attributes = 20 | Arrays.asList( 21 | "FNumber", 22 | "ExposureTime", 23 | "ISOSpeedRatings", 24 | "GPSAltitude", 25 | "GPSAltitudeRef", 26 | "FocalLength", 27 | "GPSDateStamp", 28 | "WhiteBalance", 29 | "GPSProcessingMethod", 30 | "GPSTimeStamp", 31 | "DateTime", 32 | "Flash", 33 | "GPSLatitude", 34 | "GPSLatitudeRef", 35 | "GPSLongitude", 36 | "GPSLongitudeRef", 37 | "Make", 38 | "Model", 39 | "Orientation"); 40 | for (String attribute : attributes) { 41 | setIfNotNull(oldExif, newExif, attribute); 42 | } 43 | 44 | newExif.saveAttributes(); 45 | 46 | } catch (Exception ex) { 47 | Log.e("ExifDataCopier", "Error preserving Exif data on selected image: " + ex); 48 | } 49 | } 50 | 51 | private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) { 52 | if (oldExif.getAttribute(property) != null) { 53 | newExif.setAttribute(property, oldExif.getAttribute(property)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/imagepickersaver/FileUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007-2008 OpenIntents.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * This file was modified by the Flutter authors from the following original file: 17 | * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java 18 | */ 19 | 20 | package io.flutter.plugins.imagepickersaver; 21 | 22 | import android.annotation.SuppressLint; 23 | import android.content.ContentUris; 24 | import android.content.Context; 25 | import android.database.Cursor; 26 | import android.net.Uri; 27 | import android.os.Build; 28 | import android.os.Environment; 29 | import android.provider.DocumentsContract; 30 | import android.provider.MediaStore; 31 | import android.text.TextUtils; 32 | 33 | import java.io.File; 34 | import java.io.FileOutputStream; 35 | import java.io.IOException; 36 | import java.io.InputStream; 37 | import java.io.OutputStream; 38 | 39 | class FileUtils { 40 | 41 | String getPathFromUri(final Context context, final Uri uri) { 42 | String path = getPathFromLocalUri(context, uri); 43 | if (path == null) { 44 | path = getPathFromRemoteUri(context, uri); 45 | } 46 | return path; 47 | } 48 | 49 | @SuppressLint("NewApi") 50 | private String getPathFromLocalUri(final Context context, final Uri uri) { 51 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 52 | 53 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 54 | if (isExternalStorageDocument(uri)) { 55 | final String docId = DocumentsContract.getDocumentId(uri); 56 | final String[] split = docId.split(":"); 57 | final String type = split[0]; 58 | 59 | if ("primary".equalsIgnoreCase(type)) { 60 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 61 | } 62 | } else if (isDownloadsDocument(uri)) { 63 | final String id = DocumentsContract.getDocumentId(uri); 64 | 65 | if (!TextUtils.isEmpty(id)) { 66 | try { 67 | final Uri contentUri = 68 | ContentUris.withAppendedId( 69 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); 70 | return getDataColumn(context, contentUri, null, null); 71 | } catch (NumberFormatException e) { 72 | return null; 73 | } 74 | } 75 | 76 | } else if (isMediaDocument(uri)) { 77 | final String docId = DocumentsContract.getDocumentId(uri); 78 | final String[] split = docId.split(":"); 79 | final String type = split[0]; 80 | 81 | Uri contentUri = null; 82 | if ("image".equals(type)) { 83 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 84 | } else if ("video".equals(type)) { 85 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 86 | } else if ("audio".equals(type)) { 87 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 88 | } 89 | 90 | final String selection = "_id=?"; 91 | final String[] selectionArgs = new String[]{split[1]}; 92 | 93 | return getDataColumn(context, contentUri, selection, selectionArgs); 94 | } 95 | } else if ("content".equalsIgnoreCase(uri.getScheme())) { 96 | 97 | // Return the remote address 98 | if (isGooglePhotosUri(uri)) { 99 | return uri.getLastPathSegment(); 100 | } 101 | 102 | return getDataColumn(context, uri, null, null); 103 | } else if ("file".equalsIgnoreCase(uri.getScheme())) { 104 | return uri.getPath(); 105 | } 106 | 107 | return null; 108 | } 109 | 110 | private static String getDataColumn( 111 | Context context, Uri uri, String selection, String[] selectionArgs) { 112 | Cursor cursor = null; 113 | 114 | final String column = "_data"; 115 | final String[] projection = {column}; 116 | 117 | try { 118 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); 119 | if (cursor != null && cursor.moveToFirst()) { 120 | final int column_index = cursor.getColumnIndexOrThrow(column); 121 | return cursor.getString(column_index); 122 | } 123 | } finally { 124 | if (cursor != null) { 125 | cursor.close(); 126 | } 127 | } 128 | return null; 129 | } 130 | 131 | private static String getPathFromRemoteUri(final Context context, final Uri uri) { 132 | // The code below is why Java now has try-with-resources and the Files utility. 133 | File file = null; 134 | InputStream inputStream = null; 135 | OutputStream outputStream = null; 136 | boolean success = false; 137 | try { 138 | inputStream = context.getContentResolver().openInputStream(uri); 139 | file = File.createTempFile("image_picker", "jpg", context.getCacheDir()); 140 | outputStream = new FileOutputStream(file); 141 | if (inputStream != null) { 142 | copy(inputStream, outputStream); 143 | success = true; 144 | } 145 | } catch (IOException ignored) { 146 | } finally { 147 | try { 148 | if (inputStream != null) inputStream.close(); 149 | } catch (IOException ignored) { 150 | } 151 | try { 152 | if (outputStream != null) outputStream.close(); 153 | } catch (IOException ignored) { 154 | // If closing the output stream fails, we cannot be sure that the 155 | // target file was written in full. Flushing the stream merely moves 156 | // the bytes into the OS, not necessarily to the file. 157 | success = false; 158 | } 159 | } 160 | return success ? file.getPath() : null; 161 | } 162 | 163 | private static void copy(InputStream in, OutputStream out) throws IOException { 164 | final byte[] buffer = new byte[4 * 1024]; 165 | int bytesRead; 166 | while ((bytesRead = in.read(buffer)) != -1) { 167 | out.write(buffer, 0, bytesRead); 168 | } 169 | out.flush(); 170 | } 171 | 172 | private static boolean isExternalStorageDocument(Uri uri) { 173 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 174 | } 175 | 176 | private static boolean isDownloadsDocument(Uri uri) { 177 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 178 | } 179 | 180 | private static boolean isMediaDocument(Uri uri) { 181 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 182 | } 183 | 184 | private static boolean isGooglePhotosUri(Uri uri) { 185 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/imagepickersaver/ImagePickerFileProvider.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins.imagepickersaver; 2 | 3 | 4 | import androidx.core.content.FileProvider; 5 | 6 | /** 7 | * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. 8 | *

9 | *

See https://developer.android.com/guide/topics/manifest/provider-element.html for details. 10 | */ 11 | public class ImagePickerFileProvider extends FileProvider { 12 | } 13 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/imagepickersaver/ImagePickerSaverPlugin.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package io.flutter.plugins.imagepickersaver; 6 | 7 | import android.os.Environment; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | 12 | import androidx.annotation.VisibleForTesting; 13 | import io.flutter.plugin.common.MethodCall; 14 | import io.flutter.plugin.common.MethodChannel; 15 | import io.flutter.plugin.common.PluginRegistry; 16 | 17 | 18 | public class ImagePickerSaverPlugin implements MethodChannel.MethodCallHandler { 19 | private static final String CHANNEL = "plugins.flutter.io/image_picker_saver"; 20 | 21 | private static final int SOURCE_CAMERA = 0; 22 | private static final int SOURCE_GALLERY = 1; 23 | 24 | private final PluginRegistry.Registrar registrar; 25 | private final ImagePickerDelegate delegate; 26 | 27 | public static void registerWith(PluginRegistry.Registrar registrar) { 28 | final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL); 29 | 30 | final File externalFilesDirectory = 31 | registrar.activity().getExternalFilesDir(Environment. DIRECTORY_DCIM); 32 | final ExifDataCopier exifDataCopier = new ExifDataCopier(); 33 | final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier); 34 | 35 | final ImagePickerDelegate delegate = 36 | new ImagePickerDelegate(registrar.activity(), externalFilesDirectory, imageResizer); 37 | registrar.addActivityResultListener(delegate); 38 | registrar.addRequestPermissionsResultListener(delegate); 39 | 40 | final ImagePickerSaverPlugin instance = new ImagePickerSaverPlugin(registrar, delegate); 41 | channel.setMethodCallHandler(instance); 42 | } 43 | 44 | @VisibleForTesting 45 | ImagePickerSaverPlugin(PluginRegistry.Registrar registrar, ImagePickerDelegate delegate) { 46 | this.registrar = registrar; 47 | this.delegate = delegate; 48 | } 49 | 50 | @Override 51 | public void onMethodCall(MethodCall call, MethodChannel.Result result) { 52 | if (registrar.activity() == null) { 53 | result.error("no_activity", "image_picker plugin requires a foreground activity.", null); 54 | return; 55 | } 56 | if (call.method.equals("pickImage")) { 57 | int imageSource = call.argument("source"); 58 | switch (imageSource) { 59 | case SOURCE_GALLERY: 60 | delegate.chooseImageFromGallery(call, result); 61 | break; 62 | case SOURCE_CAMERA: 63 | delegate.takeImageWithCamera(call, result); 64 | break; 65 | default: 66 | throw new IllegalArgumentException("Invalid image source: " + imageSource); 67 | } 68 | } else if (call.method.equals("pickVideo")) { 69 | int imageSource = call.argument("source"); 70 | switch (imageSource) { 71 | case SOURCE_GALLERY: 72 | delegate.chooseVideoFromGallery(call, result); 73 | break; 74 | case SOURCE_CAMERA: 75 | delegate.takeVideoWithCamera(call, result); 76 | break; 77 | default: 78 | throw new IllegalArgumentException("Invalid video source: " + imageSource); 79 | } 80 | } else if (call.method.equals("saveFile")) { 81 | 82 | 83 | try { 84 | delegate.saveImageToGallery(call, result); 85 | } catch (IOException e) { 86 | e.printStackTrace(); 87 | throw new IllegalArgumentException(e); 88 | } 89 | 90 | 91 | } else { 92 | throw new IllegalArgumentException("Unknown method " + call.method); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/imagepickersaver/ImageResizer.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package io.flutter.plugins.imagepickersaver; 6 | 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.File; 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | 15 | class ImageResizer { 16 | private final File externalFilesDirectory; 17 | private final ExifDataCopier exifDataCopier; 18 | 19 | ImageResizer(File externalFilesDirectory, ExifDataCopier exifDataCopier) { 20 | this.externalFilesDirectory = externalFilesDirectory; 21 | this.exifDataCopier = exifDataCopier; 22 | } 23 | 24 | /** 25 | * If necessary, resizes the image located in imagePath and then returns the path for the scaled 26 | * image. 27 | *

28 | *

If no resizing is needed, returns the path for the original image. 29 | */ 30 | String resizeImageIfNeeded(String imagePath, Double maxWidth, Double maxHeight) { 31 | boolean shouldScale = maxWidth != null || maxHeight != null; 32 | 33 | if (!shouldScale) { 34 | return imagePath; 35 | } 36 | 37 | try { 38 | File scaledImage = resizedImage(imagePath, maxWidth, maxHeight); 39 | exifDataCopier.copyExif(imagePath, scaledImage.getPath()); 40 | 41 | return scaledImage.getPath(); 42 | } catch (IOException e) { 43 | throw new RuntimeException(e); 44 | } 45 | } 46 | 47 | private File resizedImage(String path, Double maxWidth, Double maxHeight) throws IOException { 48 | Bitmap bmp = BitmapFactory.decodeFile(path); 49 | double originalWidth = bmp.getWidth() * 1.0; 50 | double originalHeight = bmp.getHeight() * 1.0; 51 | 52 | boolean hasMaxWidth = maxWidth != null; 53 | boolean hasMaxHeight = maxHeight != null; 54 | 55 | Double width = hasMaxWidth ? Math.min(originalWidth, maxWidth) : originalWidth; 56 | Double height = hasMaxHeight ? Math.min(originalHeight, maxHeight) : originalHeight; 57 | 58 | boolean shouldDownscaleWidth = hasMaxWidth && maxWidth < originalWidth; 59 | boolean shouldDownscaleHeight = hasMaxHeight && maxHeight < originalHeight; 60 | boolean shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight; 61 | 62 | if (shouldDownscale) { 63 | double downscaledWidth = (height / originalHeight) * originalWidth; 64 | double downscaledHeight = (width / originalWidth) * originalHeight; 65 | 66 | if (width < height) { 67 | if (!hasMaxWidth) { 68 | width = downscaledWidth; 69 | } else { 70 | height = downscaledHeight; 71 | } 72 | } else if (height < width) { 73 | if (!hasMaxHeight) { 74 | height = downscaledHeight; 75 | } else { 76 | width = downscaledWidth; 77 | } 78 | } else { 79 | if (originalWidth < originalHeight) { 80 | width = downscaledWidth; 81 | } else if (originalHeight < originalWidth) { 82 | height = downscaledHeight; 83 | } 84 | } 85 | } 86 | 87 | Bitmap scaledBmp = Bitmap.createScaledBitmap(bmp, width.intValue(), height.intValue(), false); 88 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 89 | scaledBmp.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); 90 | 91 | String[] pathParts = path.split("/"); 92 | String imageName = pathParts[pathParts.length - 1]; 93 | 94 | File imageFile = new File(externalFilesDirectory, "/scaled_" + imageName); 95 | FileOutputStream fileOutput = new FileOutputStream(imageFile); 96 | fileOutput.write(outputStream.toByteArray()); 97 | fileOutput.close(); 98 | 99 | return imageFile; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /android/src/main/res/xml/flutter_image_picker_file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 459c7fb884d9234c45b353087e0ec8a33dde90d1 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # image_picker_saver_example 2 | 3 | Demonstrates how to use the image_picker plugin. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](http://flutter.io/). 9 | -------------------------------------------------------------------------------- /example/android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_annotation_annotation_1_0_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_arch_core_core_common_2_0_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_collection_collection_1_0_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_core_core_1_0_0_aar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_common_2_0_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_runtime_2_0_0_aar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_test_espresso_espresso_core_3_1_0_aar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_test_espresso_espresso_idling_resource_3_1_0_aar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_test_monitor_1_1_0_aar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_test_runner_1_1_0_aar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__androidx_versionedparcelable_versionedparcelable_1_0_0_aar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__com_google_code_findbugs_jsr305_2_0_1_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__com_squareup_javawriter_2_1_1_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__javax_inject_javax_inject_1_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__junit_junit_4_12_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__net_bytebuddy_byte_buddy_1_8_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__net_bytebuddy_byte_buddy_agent_1_8_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__net_sf_kxml_kxml2_2_3_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__org_hamcrest_hamcrest_integration_1_3_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__org_hamcrest_hamcrest_library_1_3_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__org_mockito_mockito_core_2_17_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/.idea/libraries/Gradle__org_objenesis_objenesis_2_6_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 28 19 | 20 | lintOptions { 21 | disable 'InvalidPackage' 22 | } 23 | 24 | defaultConfig { 25 | minSdkVersion 16 26 | targetSdkVersion 28 27 | versionCode 1 28 | versionName "1" 29 | applicationId "io.flutter.plugins.imagepicker.example" 30 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig signingConfigs.debug 38 | } 39 | } 40 | 41 | testOptions { 42 | unitTests.returnDefaultValues = true 43 | } 44 | } 45 | 46 | flutter { 47 | source '../..' 48 | } 49 | 50 | dependencies { 51 | testImplementation 'junit:junit:4.12' 52 | testImplementation 'org.mockito:mockito-core:2.17.0' 53 | androidTestImplementation 'androidx.test:runner:1.1.0' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 55 | } 56 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import io.flutter.plugin.common.PluginRegistry; 4 | import io.flutter.plugins.imagepickersaver.ImagePickerSaverPlugin; 5 | import io.flutter.plugins.pathprovider.PathProviderPlugin; 6 | import io.flutter.plugins.videoplayer.VideoPlayerPlugin; 7 | 8 | /** 9 | * Generated file. Do not edit. 10 | */ 11 | public final class GeneratedPluginRegistrant { 12 | public static void registerWith(PluginRegistry registry) { 13 | if (alreadyRegisteredWith(registry)) { 14 | return; 15 | } 16 | ImagePickerSaverPlugin.registerWith(registry.registrarFor("io.flutter.plugins.imagepickersaver.ImagePickerSaverPlugin")); 17 | PathProviderPlugin.registerWith(registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); 18 | VideoPlayerPlugin.registerWith(registry.registrarFor("io.flutter.plugins.videoplayer.VideoPlayerPlugin")); 19 | } 20 | 21 | private static boolean alreadyRegisteredWith(PluginRegistry registry) { 22 | final String key = GeneratedPluginRegistrant.class.getCanonicalName(); 23 | if (registry.hasPlugin(key)) { 24 | return true; 25 | } 26 | registry.registrarFor(key); 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/io/flutter/plugins/imagepickersaverexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package io.flutter.plugins.imagepickersaverexample; 6 | 7 | import android.os.Bundle; 8 | 9 | import io.flutter.app.FlutterActivity; 10 | import io.flutter.plugins.GeneratedPluginRegistrant; 11 | 12 | public class MainActivity extends FlutterActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | GeneratedPluginRegistrant.registerWith(this); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/create/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package create.example 2 | 3 | import android.os.Bundle 4 | import io.flutter.app.FlutterActivity 5 | import io.flutter.plugins.GeneratedPluginRegistrant 6 | 7 | class MainActivity: FlutterActivity() { 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | GeneratedPluginRegistrant.registerWith(this) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins.imagepicker; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.net.Uri; 9 | 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.mockito.Mock; 13 | import org.mockito.MockitoAnnotations; 14 | 15 | import java.io.File; 16 | 17 | import io.flutter.plugin.common.MethodCall; 18 | import io.flutter.plugin.common.MethodChannel; 19 | 20 | import static org.hamcrest.core.IsEqual.equalTo; 21 | import static org.junit.Assert.assertThat; 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.verify; 25 | import static org.mockito.Mockito.verifyNoMoreInteractions; 26 | import static org.mockito.Mockito.when; 27 | 28 | public class ImagePickerDelegateTest { 29 | private static final double WIDTH = 10.0; 30 | private static final double HEIGHT = 10.0; 31 | 32 | @Mock 33 | Activity mockActivity; 34 | @Mock 35 | ImageResizer mockImageResizer; 36 | @Mock 37 | MethodCall mockMethodCall; 38 | @Mock 39 | MethodChannel.Result mockResult; 40 | @Mock 41 | ImagePickerDelegate.PermissionManager mockPermissionManager; 42 | @Mock 43 | ImagePickerDelegate.IntentResolver mockIntentResolver; 44 | @Mock 45 | FileUtils mockFileUtils; 46 | @Mock 47 | Intent mockIntent; 48 | 49 | ImagePickerDelegate.FileUriResolver mockFileUriResolver; 50 | 51 | private static class MockFileUriResolver implements ImagePickerDelegate.FileUriResolver { 52 | @Override 53 | public Uri resolveFileProviderUriForFile(String fileProviderName, File imageFile) { 54 | return null; 55 | } 56 | 57 | @Override 58 | public void getFullImagePath(Uri imageUri, ImagePickerDelegate.OnPathReadyListener listener) { 59 | listener.onPathReady("pathFromUri"); 60 | } 61 | } 62 | 63 | @Before 64 | public void setUp() { 65 | MockitoAnnotations.initMocks(this); 66 | 67 | when(mockActivity.getPackageName()).thenReturn("com.example.test"); 68 | when(mockActivity.getPackageManager()).thenReturn(mock(PackageManager.class)); 69 | 70 | when(mockFileUtils.getPathFromUri(any(Context.class), any(Uri.class))) 71 | .thenReturn("pathFromUri"); 72 | 73 | when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null)) 74 | .thenReturn("originalPath"); 75 | when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, HEIGHT)) 76 | .thenReturn("scaledPath"); 77 | when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, null)).thenReturn("scaledPath"); 78 | when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, HEIGHT)) 79 | .thenReturn("scaledPath"); 80 | 81 | mockFileUriResolver = new MockFileUriResolver(); 82 | 83 | Uri mockUri = mock(Uri.class); 84 | when(mockIntent.getData()).thenReturn(mockUri); 85 | } 86 | 87 | @Test 88 | public void whenConstructed_setsCorrectFileProviderName() { 89 | ImagePickerDelegate delegate = createDelegate(); 90 | assertThat(delegate.fileProviderName, equalTo("com.example.test.flutter.image_provider")); 91 | } 92 | 93 | @Test 94 | public void chooseImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyActiveError() { 95 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 96 | 97 | delegate.chooseImageFromGallery(mockMethodCall, mockResult); 98 | 99 | verifyFinishedWithAlreadyActiveError(); 100 | verifyNoMoreInteractions(mockResult); 101 | } 102 | 103 | @Test 104 | public void chooseImageFromGallery_WhenHasNoExternalStoragePermission_RequestsForPermission() { 105 | when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) 106 | .thenReturn(false); 107 | 108 | ImagePickerDelegate delegate = createDelegate(); 109 | delegate.chooseImageFromGallery(mockMethodCall, mockResult); 110 | 111 | verify(mockPermissionManager) 112 | .askForPermission( 113 | Manifest.permission.READ_EXTERNAL_STORAGE, 114 | ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION); 115 | } 116 | 117 | @Test 118 | public void 119 | chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() { 120 | when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) 121 | .thenReturn(true); 122 | 123 | ImagePickerDelegate delegate = createDelegate(); 124 | delegate.chooseImageFromGallery(mockMethodCall, mockResult); 125 | 126 | verify(mockActivity) 127 | .startActivityForResult( 128 | any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY)); 129 | } 130 | 131 | @Test 132 | public void takeImageWithCamera_WhenPendingResultExists_FinishesWithAlreadyActiveError() { 133 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 134 | 135 | delegate.takeImageWithCamera(mockMethodCall, mockResult); 136 | 137 | verifyFinishedWithAlreadyActiveError(); 138 | verifyNoMoreInteractions(mockResult); 139 | } 140 | 141 | @Test 142 | public void takeImageWithCamera_WhenHasNoCameraPermission_RequestsForPermission() { 143 | when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(false); 144 | 145 | ImagePickerDelegate delegate = createDelegate(); 146 | delegate.takeImageWithCamera(mockMethodCall, mockResult); 147 | 148 | verify(mockPermissionManager) 149 | .askForPermission( 150 | Manifest.permission.CAMERA, ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION); 151 | } 152 | 153 | @Test 154 | public void 155 | takeImageWithCamera_WhenHasCameraPermission_AndAnActivityCanHandleCameraIntent_LaunchesTakeWithCameraIntent() { 156 | when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); 157 | when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); 158 | 159 | ImagePickerDelegate delegate = createDelegate(); 160 | delegate.takeImageWithCamera(mockMethodCall, mockResult); 161 | 162 | verify(mockActivity) 163 | .startActivityForResult( 164 | any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA)); 165 | } 166 | 167 | @Test 168 | public void 169 | takeImageWithCamera_WhenHasCameraPermission_AndNoActivityToHandleCameraIntent_FinishesWithNoCamerasAvailableError() { 170 | when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); 171 | when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(false); 172 | 173 | ImagePickerDelegate delegate = createDelegate(); 174 | delegate.takeImageWithCamera(mockMethodCall, mockResult); 175 | 176 | verify(mockResult) 177 | .error("no_available_camera", "No cameras available for taking pictures.", null); 178 | verifyNoMoreInteractions(mockResult); 179 | } 180 | 181 | @Test 182 | public void 183 | onRequestPermissionsResult_WhenReadExternalStoragePermissionDenied_FinishesWithNull() { 184 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 185 | 186 | delegate.onRequestPermissionsResult( 187 | ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION, 188 | new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 189 | new int[]{PackageManager.PERMISSION_DENIED}); 190 | 191 | verify(mockResult).success(null); 192 | verifyNoMoreInteractions(mockResult); 193 | } 194 | 195 | @Test 196 | public void 197 | onRequestChooseImagePermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseImageFromGalleryIntent() { 198 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 199 | 200 | delegate.onRequestPermissionsResult( 201 | ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION, 202 | new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 203 | new int[]{PackageManager.PERMISSION_GRANTED}); 204 | 205 | verify(mockActivity) 206 | .startActivityForResult( 207 | any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY)); 208 | } 209 | 210 | @Test 211 | public void 212 | onRequestChooseVideoPermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseVideoFromGalleryIntent() { 213 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 214 | 215 | delegate.onRequestPermissionsResult( 216 | ImagePickerDelegate.REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION, 217 | new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 218 | new int[]{PackageManager.PERMISSION_GRANTED}); 219 | 220 | verify(mockActivity) 221 | .startActivityForResult( 222 | any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY)); 223 | } 224 | 225 | @Test 226 | public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithNull() { 227 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 228 | 229 | delegate.onRequestPermissionsResult( 230 | ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION, 231 | new String[]{Manifest.permission.CAMERA}, 232 | new int[]{PackageManager.PERMISSION_DENIED}); 233 | 234 | verify(mockResult).success(null); 235 | verifyNoMoreInteractions(mockResult); 236 | } 237 | 238 | @Test 239 | public void 240 | onRequestTakeVideoPermissionsResult_WhenCameraPermissionGranted_LaunchesTakeVideoWithCameraIntent() { 241 | when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); 242 | 243 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 244 | delegate.onRequestPermissionsResult( 245 | ImagePickerDelegate.REQUEST_CAMERA_VIDEO_PERMISSION, 246 | new String[]{Manifest.permission.CAMERA}, 247 | new int[]{PackageManager.PERMISSION_GRANTED}); 248 | 249 | verify(mockActivity) 250 | .startActivityForResult( 251 | any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA)); 252 | } 253 | 254 | @Test 255 | public void 256 | onRequestTakeImagePermissionsResult_WhenCameraPermissionGranted_LaunchesTakeWithCameraIntent() { 257 | when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); 258 | 259 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 260 | delegate.onRequestPermissionsResult( 261 | ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION, 262 | new String[]{Manifest.permission.CAMERA}, 263 | new int[]{PackageManager.PERMISSION_GRANTED}); 264 | 265 | verify(mockActivity) 266 | .startActivityForResult( 267 | any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA)); 268 | } 269 | 270 | @Test 271 | public void onActivityResult_WhenPickFromGalleryCanceled_FinishesWithNull() { 272 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 273 | 274 | delegate.onActivityResult( 275 | ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_CANCELED, null); 276 | 277 | verify(mockResult).success(null); 278 | verifyNoMoreInteractions(mockResult); 279 | } 280 | 281 | @Test 282 | public void 283 | onActivityResult_WhenImagePickedFromGallery_AndNoResizeNeeded_FinishesWithImagePath() { 284 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 285 | 286 | delegate.onActivityResult( 287 | ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); 288 | 289 | verify(mockResult).success("originalPath"); 290 | verifyNoMoreInteractions(mockResult); 291 | } 292 | 293 | @Test 294 | public void 295 | onActivityResult_WhenImagePickedFromGallery_AndResizeNeeded_FinishesWithScaledImagePath() { 296 | when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH); 297 | 298 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 299 | delegate.onActivityResult( 300 | ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); 301 | 302 | verify(mockResult).success("scaledPath"); 303 | verifyNoMoreInteractions(mockResult); 304 | } 305 | 306 | @Test 307 | public void 308 | onActivityResult_WhenVideoPickedFromGallery_AndResizeParametersSupplied_FinishesWithFilePath() { 309 | when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH); 310 | 311 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 312 | delegate.onActivityResult( 313 | ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, Activity.RESULT_OK, mockIntent); 314 | 315 | verify(mockResult).success("pathFromUri"); 316 | verifyNoMoreInteractions(mockResult); 317 | } 318 | 319 | @Test 320 | public void onActivityResult_WhenTakeImageWithCameraCanceled_FinishesWithNull() { 321 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 322 | 323 | delegate.onActivityResult( 324 | ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_CANCELED, null); 325 | 326 | verify(mockResult).success(null); 327 | verifyNoMoreInteractions(mockResult); 328 | } 329 | 330 | @Test 331 | public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_FinishesWithImagePath() { 332 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 333 | 334 | delegate.onActivityResult( 335 | ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent); 336 | 337 | verify(mockResult).success("originalPath"); 338 | verifyNoMoreInteractions(mockResult); 339 | } 340 | 341 | @Test 342 | public void 343 | onActivityResult_WhenImageTakenWithCamera_AndResizeNeeded_FinishesWithScaledImagePath() { 344 | when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH); 345 | 346 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 347 | delegate.onActivityResult( 348 | ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent); 349 | 350 | verify(mockResult).success("scaledPath"); 351 | verifyNoMoreInteractions(mockResult); 352 | } 353 | 354 | @Test 355 | public void 356 | onActivityResult_WhenVideoTakenWithCamera_AndResizeParametersSupplied_FinishesWithFilePath() { 357 | when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH); 358 | 359 | ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); 360 | delegate.onActivityResult( 361 | ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent); 362 | 363 | verify(mockResult).success("pathFromUri"); 364 | verifyNoMoreInteractions(mockResult); 365 | } 366 | 367 | private ImagePickerDelegate createDelegate() { 368 | return new ImagePickerDelegate( 369 | mockActivity, 370 | null, 371 | mockImageResizer, 372 | null, 373 | null, 374 | mockPermissionManager, 375 | mockIntentResolver, 376 | mockFileUriResolver, 377 | mockFileUtils); 378 | } 379 | 380 | private ImagePickerDelegate createDelegateWithPendingResultAndMethodCall() { 381 | return new ImagePickerDelegate( 382 | mockActivity, 383 | null, 384 | mockImageResizer, 385 | mockResult, 386 | mockMethodCall, 387 | mockPermissionManager, 388 | mockIntentResolver, 389 | mockFileUriResolver, 390 | mockFileUtils); 391 | } 392 | 393 | private void verifyFinishedWithAlreadyActiveError() { 394 | verify(mockResult).error("already_active", "Image picker is already active", null); 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins.imagepicker; 2 | 3 | import android.app.Activity; 4 | 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.rules.ExpectedException; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | import io.flutter.plugin.common.MethodCall; 16 | import io.flutter.plugin.common.MethodChannel; 17 | import io.flutter.plugin.common.PluginRegistry; 18 | 19 | import static org.mockito.Mockito.verify; 20 | import static org.mockito.Mockito.verifyZeroInteractions; 21 | import static org.mockito.Mockito.when; 22 | 23 | public class ImagePickerPluginTest { 24 | private static final int SOURCE_CAMERA = 0; 25 | private static final int SOURCE_GALLERY = 1; 26 | 27 | @Rule 28 | public ExpectedException exception = ExpectedException.none(); 29 | 30 | @Mock 31 | PluginRegistry.Registrar mockRegistrar; 32 | @Mock 33 | Activity mockActivity; 34 | @Mock 35 | ImagePickerDelegate mockImagePickerDelegate; 36 | @Mock 37 | MethodChannel.Result mockResult; 38 | 39 | ImagePickerPlugin plugin; 40 | 41 | @Before 42 | public void setUp() { 43 | MockitoAnnotations.initMocks(this); 44 | 45 | plugin = new ImagePickerPlugin(mockRegistrar, mockImagePickerDelegate); 46 | } 47 | 48 | @Test 49 | public void onMethodCall_WhenActivityIsNull_FinishesWithForegroundActivityRequiredError() { 50 | when(mockRegistrar.activity()).thenReturn(null); 51 | MethodCall call = buildMethodCall(SOURCE_GALLERY); 52 | 53 | plugin.onMethodCall(call, mockResult); 54 | 55 | verify(mockResult) 56 | .error("no_activity", "image_picker plugin requires a foreground activity.", null); 57 | verifyZeroInteractions(mockImagePickerDelegate); 58 | } 59 | 60 | @Test 61 | public void onMethodCall_WhenCalledWithUnknownMethod_ThrowsException() { 62 | when(mockRegistrar.activity()).thenReturn(mockActivity); 63 | exception.expect(IllegalArgumentException.class); 64 | exception.expectMessage("Unknown method test"); 65 | 66 | plugin.onMethodCall(new MethodCall("test", null), mockResult); 67 | 68 | verifyZeroInteractions(mockImagePickerDelegate); 69 | verifyZeroInteractions(mockResult); 70 | } 71 | 72 | @Test 73 | public void onMethodCall_WhenCalledWithUnknownImageSource_ThrowsException() { 74 | when(mockRegistrar.activity()).thenReturn(mockActivity); 75 | exception.expect(IllegalArgumentException.class); 76 | exception.expectMessage("Invalid image source: -1"); 77 | 78 | plugin.onMethodCall(buildMethodCall(-1), mockResult); 79 | 80 | verifyZeroInteractions(mockImagePickerDelegate); 81 | verifyZeroInteractions(mockResult); 82 | } 83 | 84 | @Test 85 | public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() { 86 | when(mockRegistrar.activity()).thenReturn(mockActivity); 87 | MethodCall call = buildMethodCall(SOURCE_GALLERY); 88 | 89 | plugin.onMethodCall(call, mockResult); 90 | 91 | verify(mockImagePickerDelegate).chooseImageFromGallery(call, mockResult); 92 | verifyZeroInteractions(mockResult); 93 | } 94 | 95 | @Test 96 | public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() { 97 | when(mockRegistrar.activity()).thenReturn(mockActivity); 98 | MethodCall call = buildMethodCall(SOURCE_CAMERA); 99 | 100 | plugin.onMethodCall(call, mockResult); 101 | 102 | verify(mockImagePickerDelegate).takeImageWithCamera(call, mockResult); 103 | verifyZeroInteractions(mockResult); 104 | } 105 | 106 | private MethodCall buildMethodCall(final int source) { 107 | final Map arguments = new HashMap<>(); 108 | arguments.put("source", source); 109 | 110 | return new MethodCall("pickImage", arguments); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.0' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withInputStream { stream -> plugins.load(stream) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/image_picker_example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/app.flx 37 | /Flutter/app.zip 38 | /Flutter/flutter_assets/ 39 | /Flutter/App.framework 40 | /Flutter/Flutter.framework 41 | /Flutter/Generated.xcconfig 42 | /ServiceDefinitions.json 43 | 44 | Pods/ 45 | .symlinks/ 46 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | MinimumOSVersion 28 | 8.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Generated.xcconfig: -------------------------------------------------------------------------------- 1 | // This is a generated file; do not edit or check into version control. 2 | FLUTTER_ROOT=/home/nadia/flutter 3 | FLUTTER_APPLICATION_PATH=/home/nadia/AndroidStudioProjects/image_picker_saver/example 4 | FLUTTER_TARGET=lib/main.dart 5 | FLUTTER_BUILD_DIR=build 6 | SYMROOT=${SOURCE_ROOT}/../build/ios 7 | FLUTTER_FRAMEWORK_DIR=/home/nadia/flutter/bin/cache/artifacts/engine/ios 8 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf .symlinks') 33 | system('mkdir -p .symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /example/ios/Pods/Local Podspecs/Flutter.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flutter", 3 | "version": "1.0.0", 4 | "summary": "High-performance, high-fidelity mobile apps.", 5 | "description": "Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS.", 6 | "homepage": "https://flutter.io", 7 | "license": { 8 | "type": "MIT" 9 | }, 10 | "authors": { 11 | "Flutter Dev Team": "flutter-dev@googlegroups.com" 12 | }, 13 | "source": { 14 | "git": "https://github.com/flutter/engine", 15 | "tag": "1.0.0" 16 | }, 17 | "platforms": { 18 | "ios": "7.0" 19 | }, 20 | "vendored_frameworks": "Flutter.framework" 21 | } 22 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/.gitignore: -------------------------------------------------------------------------------- 1 | GeneratedPluginRegistrant.h 2 | GeneratedPluginRegistrant.m 3 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #import 6 | #import 7 | 8 | @interface AppDelegate : FlutterAppDelegate 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "AppDelegate.h" 6 | #include "GeneratedPluginRegistrant.h" 7 | 8 | @implementation AppDelegate 9 | 10 | - (BOOL)application:(UIApplication *)application 11 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 12 | [GeneratedPluginRegistrant registerWithRegistry:self]; 13 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "idiom" : "ios-marketing", 113 | "size" : "1024x1024", 114 | "scale" : "1x" 115 | } 116 | ], 117 | "info" : { 118 | "version" : 1, 119 | "author" : "xcode" 120 | } 121 | } -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "LaunchImage.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "filename": "LaunchImage@2x.png", 11 | "scale": "2x" 12 | }, 13 | { 14 | "idiom": "universal", 15 | "filename": "LaunchImage@3x.png", 16 | "scale": "3x" 17 | } 18 | ], 19 | "info": { 20 | "version": 1, 21 | "author": "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnhefang/image_picker_saver/fccd0ab5adf5138573f0974b3384863aaee9197b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | image_picker_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | Used to demonstrate image picker plugin 27 | NSMicrophoneUsageDescription 28 | Used to capture audio for image picker plugin 29 | NSPhotoLibraryAddUsageDescription 30 | Our application needs permission to write photos... 31 | NSPhotoLibraryUsageDescription 32 | Used to demonstrate image picker plugin 33 | UIBackgroundModes 34 | 35 | fetch 36 | remote-notification 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIMainStoryboardFile 41 | Main 42 | UIRequiredDeviceCapabilities 43 | 44 | arm64 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | UIViewControllerBasedStatusBarAppearance 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #import 6 | #import 7 | #import "AppDelegate.h" 8 | 9 | int main(int argc, char* argv[]) { 10 | @autoreleasepool { 11 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/ServiceDefinitions.json: -------------------------------------------------------------------------------- 1 | {"services":[]} -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | import 'dart:typed_data'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/rendering.dart'; 10 | import 'package:http/http.dart' as http; 11 | import 'package:image_picker_saver/image_picker_saver.dart'; 12 | import 'package:video_player/video_player.dart'; 13 | import 'dart:ui' as ui; 14 | 15 | void main() { 16 | runApp(new MyApp()); 17 | } 18 | 19 | GlobalKey globalKey = new GlobalKey(); 20 | 21 | class MyApp extends StatelessWidget { 22 | @override 23 | Widget build(BuildContext context) { 24 | return new MaterialApp( 25 | title: 'Image Picker Demo', 26 | home: new MyHomePage(title: 'Image Picker Example'), 27 | ); 28 | } 29 | } 30 | 31 | class MyHomePage extends StatefulWidget { 32 | MyHomePage({Key key, this.title}) : super(key: key); 33 | 34 | final String title; 35 | 36 | @override 37 | _MyHomePageState createState() => new _MyHomePageState(); 38 | } 39 | 40 | class _MyHomePageState extends State { 41 | Future _imageFile; 42 | bool isVideo = false; 43 | VideoPlayerController _controller; 44 | VoidCallback listener; 45 | 46 | void _onImageSaveButtonPressed() async { 47 | print("_onImageSaveButtonPressed"); 48 | var response = await http 49 | .get('http://upload.art.ifeng.com/2017/0425/1493105660290.jpg'); 50 | 51 | debugPrint(response.statusCode.toString()); 52 | 53 | var filePath = await ImagePickerSaver.saveFile( 54 | fileData: response.bodyBytes, title: 'ImagePickerPicture', 55 | description: 'example of image picker saver'); 56 | 57 | var savedFile= File.fromUri(Uri.file(filePath)); 58 | setState(() { 59 | _imageFile = Future.sync(() => savedFile); 60 | }); 61 | } 62 | void _takeScreenShot() async { 63 | RenderRepaintBoundary boundary = 64 | globalKey.currentContext.findRenderObject(); 65 | ui.Image image = await boundary.toImage(); 66 | 67 | ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png); 68 | Uint8List pngBytes = byteData.buffer.asUint8List(); 69 | print(pngBytes); 70 | var filePath =await ImagePickerSaver.saveFile(fileData: pngBytes); 71 | var savedFile= File.fromUri(Uri.file(filePath)); 72 | setState(() { 73 | _imageFile = Future.sync(() => savedFile); 74 | }); 75 | } 76 | void _onImageButtonPressed(ImageSource source) { 77 | setState(() { 78 | if (_controller != null) { 79 | _controller.setVolume(0.0); 80 | _controller.removeListener(listener); 81 | } 82 | if (isVideo) { 83 | ImagePickerSaver.pickVideo(source: source).then((File file) { 84 | if (file != null && mounted) { 85 | setState(() { 86 | _controller = VideoPlayerController.file(file) 87 | ..addListener(listener) 88 | ..setVolume(1.0) 89 | ..initialize() 90 | ..setLooping(true) 91 | ..play(); 92 | }); 93 | } 94 | }); 95 | } else { 96 | _imageFile = ImagePickerSaver.pickImage(source: source); 97 | } 98 | }); 99 | } 100 | 101 | @override 102 | void deactivate() { 103 | if (_controller != null) { 104 | _controller.setVolume(0.0); 105 | _controller.removeListener(listener); 106 | } 107 | super.deactivate(); 108 | } 109 | 110 | @override 111 | void dispose() { 112 | if (_controller != null) { 113 | _controller.dispose(); 114 | } 115 | super.dispose(); 116 | } 117 | 118 | @override 119 | void initState() { 120 | super.initState(); 121 | listener = () { 122 | setState(() {}); 123 | }; 124 | } 125 | 126 | Widget _previewVideo(VideoPlayerController controller) { 127 | if (controller == null) { 128 | return const Text( 129 | 'You have not yet picked a video', 130 | textAlign: TextAlign.center, 131 | ); 132 | } else if (controller.value.initialized) { 133 | return Padding( 134 | padding: const EdgeInsets.all(10.0), 135 | child: AspectRatioVideo(controller), 136 | ); 137 | } else { 138 | return const Text( 139 | 'Error Loading Video', 140 | textAlign: TextAlign.center, 141 | ); 142 | } 143 | } 144 | 145 | Widget _previewImage() { 146 | return FutureBuilder( 147 | future: _imageFile, 148 | builder: (BuildContext context, AsyncSnapshot snapshot) { 149 | if (snapshot.connectionState == ConnectionState.done && 150 | snapshot.data != null) { 151 | debugPrint(snapshot.data.path); 152 | return Image.file(snapshot.data); 153 | } else if (snapshot.error != null) { 154 | return const Text( 155 | 'Error picking image.', 156 | textAlign: TextAlign.center, 157 | ); 158 | } else { 159 | return const Text( 160 | 'You have not yet picked an image.', 161 | textAlign: TextAlign.center, 162 | ); 163 | } 164 | }); 165 | } 166 | 167 | @override 168 | Widget build(BuildContext context) { 169 | return new RepaintBoundary( 170 | key: globalKey, 171 | child: Scaffold( 172 | appBar: AppBar( 173 | title: Text(widget.title), 174 | ), 175 | body: Center( 176 | child: isVideo ? _previewVideo(_controller) : _previewImage(), 177 | ), 178 | floatingActionButton: Column( 179 | mainAxisAlignment: MainAxisAlignment.end, 180 | children: [ 181 | FloatingActionButton( 182 | onPressed: () { 183 | isVideo = false; 184 | _onImageButtonPressed(ImageSource.gallery); 185 | }, 186 | heroTag: 'image0', 187 | tooltip: 'Pick Image from gallery', 188 | child: const Icon(Icons.photo_library), 189 | ), 190 | Padding( 191 | padding: const EdgeInsets.only(top: 16.0), 192 | child: FloatingActionButton( 193 | onPressed: () { 194 | isVideo = false; 195 | _onImageButtonPressed(ImageSource.camera); 196 | }, 197 | heroTag: 'image1', 198 | tooltip: 'Take a Photo', 199 | child: const Icon(Icons.camera_alt), 200 | ), 201 | ), 202 | Padding( 203 | padding: const EdgeInsets.only(top: 16.0), 204 | child: FloatingActionButton( 205 | backgroundColor: Colors.red, 206 | onPressed: () { 207 | isVideo = true; 208 | _onImageButtonPressed(ImageSource.gallery); 209 | }, 210 | heroTag: 'video0', 211 | tooltip: 'Pick Video from gallery', 212 | child: const Icon(Icons.video_library), 213 | ), 214 | ), 215 | Padding( 216 | padding: const EdgeInsets.only(top: 16.0), 217 | child: FloatingActionButton( 218 | backgroundColor: Colors.red, 219 | onPressed: () { 220 | isVideo = true; 221 | _onImageButtonPressed(ImageSource.camera); 222 | }, 223 | heroTag: 'video1', 224 | tooltip: 'Take a Video', 225 | child: const Icon(Icons.videocam), 226 | ), 227 | ), 228 | Padding( 229 | padding: const EdgeInsets.only(top: 16.0), 230 | child: FloatingActionButton( 231 | backgroundColor: Colors.lightGreen, 232 | onPressed: () { 233 | _onImageSaveButtonPressed(); 234 | }, 235 | heroTag: 'save image from url', 236 | tooltip: 'save image from url', 237 | child: const Icon(Icons.save), 238 | ), 239 | ), 240 | Padding( 241 | padding: const EdgeInsets.only(top: 16.0), 242 | child: FloatingActionButton( 243 | backgroundColor: Colors.lightGreen, 244 | onPressed: () { 245 | _takeScreenShot(); 246 | }, 247 | heroTag: 'ScreenShot', 248 | tooltip: 'ScreenShot', 249 | child: const Icon(Icons.mobile_screen_share), 250 | ), 251 | ), 252 | ], 253 | ), 254 | )); 255 | } 256 | } 257 | 258 | class AspectRatioVideo extends StatefulWidget { 259 | final VideoPlayerController controller; 260 | 261 | AspectRatioVideo(this.controller); 262 | 263 | @override 264 | AspectRatioVideoState createState() => new AspectRatioVideoState(); 265 | } 266 | 267 | class AspectRatioVideoState extends State { 268 | VideoPlayerController get controller => widget.controller; 269 | bool initialized = false; 270 | 271 | VoidCallback listener; 272 | 273 | @override 274 | void initState() { 275 | super.initState(); 276 | listener = () { 277 | if (!mounted) { 278 | return; 279 | } 280 | if (initialized != controller.value.initialized) { 281 | initialized = controller.value.initialized; 282 | setState(() {}); 283 | } 284 | }; 285 | controller.addListener(listener); 286 | } 287 | 288 | @override 289 | Widget build(BuildContext context) { 290 | if (initialized) { 291 | final Size size = controller.value.size; 292 | return new Center( 293 | child: new AspectRatio( 294 | aspectRatio: size.width / size.height, 295 | child: new VideoPlayer(controller), 296 | ), 297 | ); 298 | } else { 299 | return new Container(); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /example/lib/mian_screenhot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/rendering.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:image_picker_saver/image_picker_saver.dart'; 8 | 9 | void main() => runApp(new MyApp()); 10 | 11 | class MyApp extends StatelessWidget { 12 | // This widget is the root of your application. 13 | @override 14 | Widget build(BuildContext context) { 15 | return new MaterialApp( 16 | title: 'Flutter Demo', 17 | theme: new ThemeData( 18 | primarySwatch: Colors.blue, 19 | ), 20 | home: new MyHomePage(title: 'Flutter Demo Home Page'), 21 | ); 22 | } 23 | } 24 | 25 | class MyHomePage extends StatefulWidget { 26 | MyHomePage({Key key, this.title}) : super(key: key); 27 | final String title; 28 | 29 | @override 30 | _MyHomePageState createState() => new _MyHomePageState(); 31 | } 32 | 33 | class _MyHomePageState extends State { 34 | static GlobalKey previewContainer = new GlobalKey(); 35 | int _counter = 0; 36 | 37 | void _incrementCounter() { 38 | setState(() { 39 | // This call to setState tells the Flutter framework that something has 40 | // changed in this State, which causes it to rerun the build method below 41 | // so that the display can reflect the updated values. If we changed 42 | // _counter without calling setState(), then the build method would not be 43 | // called again, and so nothing would appear to happen. 44 | _counter++; 45 | }); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return RepaintBoundary( 51 | key: previewContainer, 52 | child: new Scaffold( 53 | appBar: new AppBar( 54 | title: new Text(widget.title), 55 | ), 56 | body: new Center( 57 | child: new Column( 58 | mainAxisAlignment: MainAxisAlignment.center, 59 | children: [ 60 | new Text( 61 | 'You have pushed the button this many times:', 62 | ), 63 | new Text( 64 | '$_counter', 65 | style: Theme.of(context).textTheme.display1, 66 | ), 67 | new RaisedButton( 68 | onPressed: takeScreenShot, 69 | child: const Text('Take a Screenshot'), 70 | ), 71 | ], 72 | ), 73 | ), 74 | floatingActionButton: new FloatingActionButton( 75 | onPressed: _incrementCounter, 76 | tooltip: 'Increment', 77 | child: new Icon(Icons.add), 78 | ), // This trailing comma makes auto-formatting nicer for build methods. 79 | )); 80 | } 81 | 82 | takeScreenShot() async { 83 | RenderRepaintBoundary boundary = 84 | previewContainer.currentContext.findRenderObject(); 85 | ui.Image image = await boundary.toImage(); 86 | 87 | ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png); 88 | Uint8List pngBytes = byteData.buffer.asUint8List(); 89 | print(pngBytes); 90 | ImagePickerSaver.saveFile(fileData: pngBytes); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: image_picker_example 2 | description: Demonstrates how to use the image_picker plugin. 3 | 4 | dependencies: 5 | video_player: ^0.10.0 6 | http: ^0.11.3+15 7 | path_provider: ^0.5.0 8 | flutter: 9 | sdk: flutter 10 | image_picker_saver: 11 | path: ../ 12 | 13 | dev_dependencies: 14 | flutter_test: 15 | sdk: flutter 16 | # For information on the generic Dart part of this file, see the 17 | # following page: https://www.dartlang.org/tools/pub/pubspec 18 | 19 | # The following section is specific to Flutter. 20 | flutter: 21 | 22 | # The following line ensures that the Material Icons font is 23 | # included with your application, so that you can use the icons in 24 | # the Icons class. 25 | uses-material-design: true 26 | 27 | # To add assets to your application, add an assets section here, in 28 | # this "flutter" section, as in: 29 | # assets: 30 | # - images/a_dot_burr.jpeg 31 | # - images/a_dot_ham.jpeg 32 | 33 | # To add custom fonts to your application, add a fonts section here, 34 | # in this "flutter" section. Each entry in this list should have a 35 | # "family" key with the font family name, and a "fonts" key with a 36 | # list giving the asset and other descriptors for the font. For 37 | # example: 38 | # fonts: 39 | # - family: Schyler 40 | # fonts: 41 | # - asset: fonts/Schyler-Regular.ttf 42 | # - asset: fonts/Schyler-Italic.ttf 43 | # style: italic 44 | # - family: Trajan Pro 45 | # fonts: 46 | # - asset: fonts/TrajanPro.ttf 47 | # - asset: fonts/TrajanPro_Bold.ttf 48 | # weight: 700 49 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import 'package:image_picker_example/main.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(new MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | example 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /ios/Classes/ImagePickerSaverPlugin.h: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #import 6 | 7 | @interface FLTImagePickerSaverPlugin : NSObject 8 | @end 9 | -------------------------------------------------------------------------------- /ios/Classes/ImagePickerSaverPlugin.m: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #import "ImagePickerSaverPlugin.h" 6 | 7 | #import 8 | #import 9 | #import 10 | 11 | 12 | @interface FLTImagePickerSaverPlugin () 13 | @end 14 | 15 | static const int SOURCE_CAMERA = 0; 16 | static const int SOURCE_GALLERY = 1; 17 | 18 | @implementation FLTImagePickerSaverPlugin { 19 | FlutterResult _result; 20 | NSDictionary *_arguments; 21 | UIImagePickerController *_imagePickerController; 22 | UIViewController *_viewController; 23 | } 24 | 25 | + (void)registerWithRegistrar:(NSObject *)registrar { 26 | FlutterMethodChannel *channel = 27 | [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker_saver" 28 | binaryMessenger:[registrar messenger]]; 29 | UIViewController *viewController = 30 | [UIApplication sharedApplication].delegate.window.rootViewController; 31 | FLTImagePickerSaverPlugin *instance = 32 | [[FLTImagePickerSaverPlugin alloc] initWithViewController:viewController]; 33 | [registrar addMethodCallDelegate:instance channel:channel]; 34 | } 35 | 36 | - (instancetype)initWithViewController:(UIViewController *)viewController { 37 | self = [super init]; 38 | if (self) { 39 | _viewController = viewController; 40 | _imagePickerController = [[UIImagePickerController alloc] init]; 41 | } 42 | return self; 43 | } 44 | 45 | - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { 46 | if (_result) { 47 | _result([FlutterError errorWithCode:@"multiple_request" 48 | message:@"Cancelled by a second request" 49 | details:nil]); 50 | _result = nil; 51 | } 52 | 53 | if ([@"pickImage" isEqualToString:call.method]) { 54 | _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; 55 | _imagePickerController.delegate = self; 56 | _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; 57 | 58 | _result = result; 59 | _arguments = call.arguments; 60 | 61 | int imageSource = [[_arguments objectForKey:@"source"] intValue]; 62 | 63 | switch (imageSource) { 64 | case SOURCE_CAMERA: 65 | [self showCamera]; 66 | break; 67 | case SOURCE_GALLERY: 68 | [self showPhotoLibrary]; 69 | break; 70 | default: 71 | result([FlutterError errorWithCode:@"invalid_source" 72 | message:@"Invalid image source." 73 | details:nil]); 74 | break; 75 | } 76 | } else if ([@"pickVideo" isEqualToString:call.method]) { 77 | _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; 78 | _imagePickerController.delegate = self; 79 | _imagePickerController.mediaTypes = @[ 80 | (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, 81 | (NSString *)kUTTypeMPEG4 82 | ]; 83 | _imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; 84 | 85 | _result = result; 86 | _arguments = call.arguments; 87 | 88 | int imageSource = [[_arguments objectForKey:@"source"] intValue]; 89 | 90 | switch (imageSource) { 91 | case SOURCE_CAMERA: 92 | [self showCamera]; 93 | break; 94 | case SOURCE_GALLERY: 95 | [self showPhotoLibrary]; 96 | break; 97 | default: 98 | result([FlutterError errorWithCode:@"invalid_source" 99 | message:@"Invalid video source." 100 | details:nil]); 101 | break; 102 | } 103 | } else if ([@"saveFile" isEqualToString:call.method]) { 104 | _result = result; 105 | _arguments = call.arguments; 106 | 107 | FlutterStandardTypedData* fileData = [_arguments objectForKey:@"fileData"] ; 108 | 109 | NSString * fileName=@""; 110 | //NSLog(@"fileData.data.length :%ul",fileData.data.length); 111 | UIImage *image=[UIImage imageWithData:fileData.data]; 112 | 113 | PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; 114 | if (status == PHAuthorizationStatusRestricted) { 115 | NSLog(@"not allow to access photo library"); 116 | } else if (status == PHAuthorizationStatusDenied) { // if user chosen"Not Allow" 117 | NSLog(@"提Remind users to go to [Settings - Privacy - Photo - xxx] to open the access switch"); 118 | } else if (status == PHAuthorizationStatusAuthorized) { // if user chosen"Allow" 119 | [self saveImage:image]; 120 | } else if (status == PHAuthorizationStatusNotDetermined) { // if user not chosen before 121 | // Requests authorization with dialog 122 | [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { 123 | if (status == PHAuthorizationStatusAuthorized) { // if user chosen "Allow" 124 | //Save Image to Directory 125 | [self saveImage:image]; 126 | } 127 | }]; 128 | } 129 | //_result(fileName); 130 | } 131 | else { 132 | result(FlutterMethodNotImplemented); 133 | } 134 | } 135 | 136 | -(void)saveImage:(UIImage *)image { 137 | __block NSString* fileName; 138 | __block NSString* localId; 139 | __block PHAssetChangeRequest *assetChangeRequest = nil; 140 | [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ 141 | PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image]; 142 | 143 | 144 | // [assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]]; 145 | 146 | localId = [[assetChangeRequest placeholderForCreatedAsset] localIdentifier]; 147 | } completionHandler:^(BOOL success, NSError *error) { 148 | 149 | if (success) { 150 | NSLog(@"save image successful "); 151 | PHFetchResult* assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localId] options:nil]; 152 | PHAsset *asset = [assetResult firstObject]; 153 | 154 | if (@available(iOS 13.0, *)) { 155 | [asset requestContentEditingInputWithOptions:nil completionHandler:^(PHContentEditingInput * _Nullable contentEditingInput, NSDictionary * _Nonnull info) { 156 | fileName= contentEditingInput.fullSizeImageURL.absoluteString; 157 | _result(fileName); 158 | }]; 159 | } else { 160 | [[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { 161 | NSLog(@"Success %@ %@",dataUTI,info); 162 | 163 | NSLog(@"Success PHImageFileURLKey %@ ", (NSString *)[info objectForKey:@"PHImageFileURLKey"]); 164 | fileName=((NSURL *)[info objectForKey:@"PHImageFileURLKey"]).absoluteString; 165 | _result(fileName); 166 | }]; 167 | } 168 | 169 | } else { 170 | NSLog(@"save image failed!%@",error); 171 | 172 | fileName= @""; 173 | _result(fileName); 174 | 175 | } 176 | }]; 177 | } 178 | 179 | 180 | 181 | 182 | - (void)showCamera { 183 | // Camera is not available on simulators 184 | if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { 185 | _imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera; 186 | [_viewController presentViewController:_imagePickerController animated:YES completion:nil]; 187 | } else { 188 | [[[UIAlertView alloc] initWithTitle:@"Error" 189 | message:@"Camera not available." 190 | delegate:nil 191 | cancelButtonTitle:@"OK" 192 | otherButtonTitles:nil] show]; 193 | } 194 | } 195 | 196 | - (void)showPhotoLibrary { 197 | // No need to check if SourceType is available. It always is. 198 | _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; 199 | [_viewController presentViewController:_imagePickerController animated:YES completion:nil]; 200 | } 201 | 202 | - (void)imagePickerController:(UIImagePickerController *)picker 203 | didFinishPickingMediaWithInfo:(NSDictionary *)info { 204 | NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL]; 205 | UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage]; 206 | [_imagePickerController dismissViewControllerAnimated:YES completion:nil]; 207 | 208 | if (videoURL != nil) { 209 | NSData *data = [NSData dataWithContentsOfURL:videoURL]; 210 | NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString]; 211 | NSString *tmpFile = [NSString stringWithFormat:@"image_picker_saver_%@.MOV", guid]; 212 | NSString *tmpDirectory = NSTemporaryDirectory(); 213 | NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile]; 214 | 215 | if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:data attributes:nil]) { 216 | _result(tmpPath); 217 | } else { 218 | _result([FlutterError errorWithCode:@"create_error" 219 | message:@"Temporary file could not be created" 220 | details:nil]); 221 | } 222 | } else { 223 | if (image == nil) { 224 | image = [info objectForKey:UIImagePickerControllerOriginalImage]; 225 | } 226 | image = [self normalizedImage:image]; 227 | 228 | NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"]; 229 | NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; 230 | 231 | if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { 232 | image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight]; 233 | } 234 | 235 | NSData *data = UIImageJPEGRepresentation(image, 1.0); 236 | NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString]; 237 | NSString *tmpFile = [NSString stringWithFormat:@"image_picker_saver_%@.jpg", guid]; 238 | NSString *tmpDirectory = NSTemporaryDirectory(); 239 | NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile]; 240 | 241 | if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:data attributes:nil]) { 242 | _result(tmpPath); 243 | } else { 244 | _result([FlutterError errorWithCode:@"create_error" 245 | message:@"Temporary file could not be created" 246 | details:nil]); 247 | } 248 | } 249 | 250 | _result = nil; 251 | _arguments = nil; 252 | } 253 | 254 | - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { 255 | [_imagePickerController dismissViewControllerAnimated:YES completion:nil]; 256 | _result(nil); 257 | 258 | _result = nil; 259 | _arguments = nil; 260 | } 261 | 262 | // The way we save images to the tmp dir currently throws away all EXIF data 263 | // (including the orientation of the image). That means, pics taken in portrait 264 | // will not be orientated correctly as is. To avoid that, we rotate the actual 265 | // image data. 266 | // TODO(goderbauer): investigate how to preserve EXIF data. 267 | - (UIImage *)normalizedImage:(UIImage *)image { 268 | if (image.imageOrientation == UIImageOrientationUp) return image; 269 | 270 | UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); 271 | [image drawInRect:(CGRect){0, 0, image.size}]; 272 | UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext(); 273 | UIGraphicsEndImageContext(); 274 | return normalizedImage; 275 | } 276 | 277 | - (UIImage *)scaledImage:(UIImage *)image 278 | maxWidth:(NSNumber *)maxWidth 279 | maxHeight:(NSNumber *)maxHeight { 280 | double originalWidth = image.size.width; 281 | double originalHeight = image.size.height; 282 | 283 | bool hasMaxWidth = maxWidth != (id)[NSNull null]; 284 | bool hasMaxHeight = maxHeight != (id)[NSNull null]; 285 | 286 | double width = hasMaxWidth ? MIN([maxWidth doubleValue], originalWidth) : originalWidth; 287 | double height = hasMaxHeight ? MIN([maxHeight doubleValue], originalHeight) : originalHeight; 288 | 289 | bool shouldDownscaleWidth = hasMaxWidth && [maxWidth doubleValue] < originalWidth; 290 | bool shouldDownscaleHeight = hasMaxHeight && [maxHeight doubleValue] < originalHeight; 291 | bool shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight; 292 | 293 | if (shouldDownscale) { 294 | double downscaledWidth = (height / originalHeight) * originalWidth; 295 | double downscaledHeight = (width / originalWidth) * originalHeight; 296 | 297 | if (width < height) { 298 | if (!hasMaxWidth) { 299 | width = downscaledWidth; 300 | } else { 301 | height = downscaledHeight; 302 | } 303 | } else if (height < width) { 304 | if (!hasMaxHeight) { 305 | height = downscaledHeight; 306 | } else { 307 | width = downscaledWidth; 308 | } 309 | } else { 310 | if (originalWidth < originalHeight) { 311 | width = downscaledWidth; 312 | } else if (originalHeight < originalWidth) { 313 | height = downscaledHeight; 314 | } 315 | } 316 | } 317 | 318 | UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0); 319 | [image drawInRect:CGRectMake(0, 0, width, height)]; 320 | 321 | UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); 322 | UIGraphicsEndImageContext(); 323 | 324 | return scaledImage; 325 | } 326 | 327 | 328 | 329 | @end 330 | -------------------------------------------------------------------------------- /ios/image_picker_saver.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'image_picker_saver' 6 | s.version = '0.0.1' 7 | s.summary = 'Flutter plugin that shows an image picker and save image to photo library.' 8 | s.description = <<-DESC 9 | Flutter plugin that shows an image picker. 10 | DESC 11 | s.homepage = 'https://github.com/cnhefang/image_picker_saver' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Aaron Ho' => 'cnhefang@outlook.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | end 19 | -------------------------------------------------------------------------------- /lib/image_picker_saver.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | import 'dart:typed_data'; 8 | 9 | import 'package:flutter/foundation.dart'; 10 | import 'package:flutter/services.dart'; 11 | 12 | /// Specifies the source where the picked image should come from. 13 | enum ImageSource { 14 | /// Opens up the device camera, letting the user to take a new picture. 15 | camera, 16 | 17 | /// Opens the user's photo gallery. 18 | gallery, 19 | } 20 | 21 | class ImagePickerSaver { 22 | static const MethodChannel _channel = 23 | MethodChannel('plugins.flutter.io/image_picker_saver'); 24 | 25 | /// Returns a [File] object pointing to the image that was picked. 26 | /// 27 | /// The [source] argument controls where the image comes from. This can 28 | /// be either [ImageSource.camera] or [ImageSource.gallery]. 29 | /// 30 | /// If specified, the image will be at most [maxWidth] wide and 31 | /// [maxHeight] tall. Otherwise the image will be returned at it's 32 | /// original width and height. 33 | static Future pickImage({ 34 | @required ImageSource source, 35 | double maxWidth, 36 | double maxHeight, 37 | }) async { 38 | assert(source != null); 39 | 40 | if (maxWidth != null && maxWidth < 0) { 41 | throw new ArgumentError.value(maxWidth, 'maxWidth cannot be negative'); 42 | } 43 | 44 | if (maxHeight != null && maxHeight < 0) { 45 | throw new ArgumentError.value(maxHeight, 'maxHeight cannot be negative'); 46 | } 47 | 48 | final String path = await _channel.invokeMethod( 49 | 'pickImage', 50 | { 51 | 'source': source.index, 52 | 'maxWidth': maxWidth, 53 | 'maxHeight': maxHeight, 54 | }, 55 | ); 56 | 57 | return path == null ? null : new File(path); 58 | } 59 | 60 | static Future pickVideo({ 61 | @required ImageSource source, 62 | }) async { 63 | assert(source != null); 64 | 65 | final String path = await _channel.invokeMethod( 66 | 'pickVideo', 67 | { 68 | 'source': source.index, 69 | }, 70 | ); 71 | return path == null ? null : new File(path); 72 | } 73 | 74 | static Future saveFile({@required Uint8List fileData, String title, String description}) async { 75 | assert(fileData != null); 76 | 77 | String filePath = await _channel.invokeMethod( 78 | 'saveFile', 79 | { 80 | 'fileData': fileData, 81 | 'title': title, 82 | 'description': description 83 | }, 84 | ); 85 | debugPrint("saved filePath:" + filePath); 86 | //process ios return filePath 87 | if(filePath.startsWith("file://")){ 88 | filePath=filePath.replaceAll("file://", ""); 89 | } 90 | return filePath; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: image_picker_saver 2 | description: 3 | Image Picker and Saver plugin for Flutter 4 | Android X supported 5 | IOS supported 8.0+ 6 | fork form official plugin image_picker' 0.4.6 7 | and add saver to save image to gallery 8 | 9 | authors: 10 | - Aaron Ho 11 | 12 | homepage: https://github.com/cnhefang/image_picker_saver 13 | version: 0.3.0 14 | 15 | flutter: 16 | plugin: 17 | androidPackage: io.flutter.plugins.imagepickersaver 18 | iosPrefix: FLT 19 | pluginClass: ImagePickerSaverPlugin 20 | 21 | dependencies: 22 | flutter: 23 | sdk: flutter 24 | 25 | dev_dependencies: 26 | flutter_test: 27 | sdk: flutter 28 | 29 | environment: 30 | sdk: ">=2.0.0-dev.28.0 <3.0.0" 31 | flutter: ">=0.1.4 <2.0.0" 32 | -------------------------------------------------------------------------------- /test/image_picker_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:image_picker_saver/image_picker_saver.dart'; 8 | 9 | void main() { 10 | group('$ImagePickerSaver', () { 11 | const MethodChannel channel = 12 | MethodChannel('plugins.flutter.io/image_picker_saver'); 13 | 14 | final List log = []; 15 | 16 | setUp(() { 17 | channel.setMockMethodCallHandler((MethodCall methodCall) async { 18 | log.add(methodCall); 19 | return ''; 20 | }); 21 | 22 | log.clear(); 23 | }); 24 | 25 | group('#pickImage', () { 26 | test('passes the image source argument correctly', () async { 27 | await ImagePickerSaver.pickImage(source: ImageSource.camera); 28 | await ImagePickerSaver.pickImage(source: ImageSource.gallery); 29 | 30 | expect( 31 | log, 32 | [ 33 | isMethodCall('pickImage', arguments: { 34 | 'source': 0, 35 | 'maxWidth': null, 36 | 'maxHeight': null, 37 | }), 38 | isMethodCall('pickImage', arguments: { 39 | 'source': 1, 40 | 'maxWidth': null, 41 | 'maxHeight': null, 42 | }), 43 | ], 44 | ); 45 | }); 46 | 47 | test('passes the width and height arguments correctly', () async { 48 | await ImagePickerSaver.pickImage(source: ImageSource.camera); 49 | await ImagePickerSaver.pickImage( 50 | source: ImageSource.camera, 51 | maxWidth: 10.0, 52 | ); 53 | await ImagePickerSaver.pickImage( 54 | source: ImageSource.camera, 55 | maxHeight: 10.0, 56 | ); 57 | await ImagePickerSaver.pickImage( 58 | source: ImageSource.camera, 59 | maxWidth: 10.0, 60 | maxHeight: 20.0, 61 | ); 62 | 63 | expect( 64 | log, 65 | [ 66 | isMethodCall('pickImage', arguments: { 67 | 'source': 0, 68 | 'maxWidth': null, 69 | 'maxHeight': null, 70 | }), 71 | isMethodCall('pickImage', arguments: { 72 | 'source': 0, 73 | 'maxWidth': 10.0, 74 | 'maxHeight': null, 75 | }), 76 | isMethodCall('pickImage', arguments: { 77 | 'source': 0, 78 | 'maxWidth': null, 79 | 'maxHeight': 10.0, 80 | }), 81 | isMethodCall('pickImage', arguments: { 82 | 'source': 0, 83 | 'maxWidth': 10.0, 84 | 'maxHeight': 20.0, 85 | }), 86 | ], 87 | ); 88 | }); 89 | 90 | test('does not accept a negative width or height argument', () { 91 | expect( 92 | ImagePickerSaver.pickImage(source: ImageSource.camera, maxWidth: -1.0), 93 | throwsArgumentError, 94 | ); 95 | 96 | expect( 97 | ImagePickerSaver.pickImage(source: ImageSource.camera, maxHeight: -1.0), 98 | throwsArgumentError, 99 | ); 100 | }); 101 | 102 | test('handles a null image path response gracefully', () async { 103 | channel.setMockMethodCallHandler((MethodCall methodCall) => null); 104 | 105 | expect( 106 | await ImagePickerSaver.pickImage(source: ImageSource.gallery), isNull); 107 | expect(await ImagePickerSaver.pickImage(source: ImageSource.camera), isNull); 108 | }); 109 | }); 110 | }); 111 | } 112 | --------------------------------------------------------------------------------